方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【Flutter】WebViewでモーダル表示してスクロール可能にする方法

この記事は約15分で読めます。

はじめに

現在、方言を話すおしゃべり猫型ロボット「ミーア」を開発中。

ミーア
おしゃべり猫型ロボット「ミーア」は、100以上の種類の豊かな表情で、悲しみや喜びを共有します。様々な性格(皮肉・おせっかい・ロマンチスト・天然)や方言(大阪弁・博多弁・鹿児島弁・広島弁)も話してくれます。ミーアとの暮らしで、毎日の生活をもっ...

ミーアのWebサイトを作成して、利用規約・プラポリも作成したので、今回はこれらをflutterアプリからもアクセスできるようにする。

ミーアwebサイト:https://mia-cat.com/
利用規約:https://mia-cat.com/terms-of-service/
プラポリ:https://mia-cat.com/privacy-policy/

そのまま利用規約・プラポリのwebページにアプリから外部遷移するのもありだとは思うが、アプリ内WebView表示の方が若干UX上がるだろうということで。

アプリ内にユーザーの設定画面があり、その中に、利用規約・プラポリの項目を用意しているので、この項目をタップした時にWebViewで各内容が表示され、閉じることもできるようにする

利用規約・プラポリのWebサイトでの表示

notion埋め込みを試みたが、、、

いきなり閑話休題。まずWebサイトに利用規約・プラポリを表示する際に、notionで作成して、それをHP内に埋め込みできれば、更新作業はnotion内の文章を変更するだけなので楽だなと思い、方法を探したところ、notionで記事を作成・公開したものをiframeやwordpressのショートコードで埋め込めるサービスをいくつか発見。実際に試してみて、正常動作することを確認できた。

例)Embed Notion Pages(Wordpressプラグインあり):ただし有料

https://www.embednotionpages.com/embeds

しかし、どのサービスも有料サービスで『単に埋め込むだけのサービスでお金取られてもなぁ。。。』と思い、そんなに更新することもないだろうと思い、結局今回はwordpressの固定ページにベタ張りした。ちなみにgoogle documentの場合は埋め込み表示できて無料で更新もできる。特に追加のプラグインやサービスも不要。

今後、FAQやマニュアルに関してもnotionに集約していきたいので、もしかしたら有料のnotion embedサービスを使うことになるかもしれない。もし、無料でできる方法(自前での実装含めて)ありましたらどなたか教えていただけますと幸いですm(_ _)m

Flutterプラグイン:webview_flutterを導入

Flutterでwebviewを提供しているプラグインはいくつかあるが、大きく2つが候補に上がる。

  • webview_flutter

    →基本的なWebビュー機能を提供し、URLの読み込みやJavaScriptの実行など、最も一般的な用途に対応。シンプルな実装であればこれで必要十分

  • flutter_inappwebview

    →webview_flutterより高度な機能を提供するサードパーティ製のWebビューパッケージ

    JavaScriptチャネルのサポート、HTTPリクエストのオーバーライド、カスタムコンテキストメニューの作成、ユーザーエージェントのカスタマイズなど、webview_flutterよりも多くの機能を提供している。

今回は、シンプルな実装で事足りるので、webview_flutterを導入する

ちなみに「Flutter アプリに WebView を追加する」というタイトルのGoogle Codelabs(様々な実装のガイドラインやチュートリアルをハンズオン形式で学ぶコンテンツ)があった。

Flutter アプリに WebView を追加する  |  Google Codelabs
この Codelab では、webview_flutter プラグインを Flutter アプリに追加する方法を学びます。
ShellScript
$ flutter pub add webview_flutter
$ flutter pub get

Android で webview_flutter プラグインを使用するには、使用する Android プラットフォームのビューに応じて、minSDK を 19 または 20 に設定する必要があるので、minSDKが必要条件を満たしていない場合にはgradleファイルを修正する。

Dart
// android/app/build.gradle
defaultConfig {
    // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
    applicationId "com.example.webview_in_flutter"
    minSdkVersion 20        // MODIFY
    targetSdkVersion 30
    versionCode flutterVersionCode.toInteger()
    versionName flutterVersionName
}

利用規約・プラポリを表示するための新しい画面を作成

webview_flutterパッケージのUsageを参照。

webview_flutter | Flutter package
A Flutter plugin that provides a WebView widget on Android and iOS.

WebViewController のインスタンスを作成し、そのインスタンスを WebViewWidget に引数として渡して、Scaffoldbody 内で WebViewWidget を表示する。

WebViewController のインスタンスを作成する際に、メソッドをいくつか設定できる。

  • setJavaScriptMode(JavaScriptMode.unrestricted):WebView 内での JavaScript の実行を許可
  • setBackgroundColor(const Color(0x00000000)):WebView の背景色を設定。
  • setNavigationDelegate(NavigationDelegate(...)):
  • ページの読み込み開始、読み込み完了、エラー発生時の動作や、特定のナビゲーションリクエストを許可または拒否することができる。例えば、onNavigationRequest では、特定の URL へのナビゲーションを制御している。ここでは、URL が https://www.youtube.com/ で始まる場合にナビゲーションを阻止している。
  • loadRequest(Uri.parse('https://flutter.dev')): 指定された URL の Web ページを WebView でロードする。この例では https://flutter.dev をロードしている。
Dart
controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted)
  ..setBackgroundColor(const Color(0x00000000))
  ..setNavigationDelegate(
    NavigationDelegate(
      onProgress: (int progress) {
        // Update loading bar.
      },
      onPageStarted: (String url) {},
      onPageFinished: (String url) {},
      onWebResourceError: (WebResourceError error) {},
      onNavigationRequest: (NavigationRequest request) {
        if (request.url.startsWith('https://www.youtube.com/')) {
          return NavigationDecision.prevent;
        }
        return NavigationDecision.navigate;
      },
    ),
  )
  ..loadRequest(Uri.parse('https://flutter.dev'));

今回は、利用規約のURL(https://mia-cat.com/terms-of-service)を表示するだけのシンプルなものなので、loadRequest メソッドのみを使用して URL を読み込むようにする。

Dart
// lib/screens/home/terms_of_service_screen.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class TermsOfServiceScreen extends StatefulWidget {
  const TermsOfServiceScreen({Key? key}) : super(key: key);

  @override
  _TermsOfServiceScreenState createState() => _TermsOfServiceScreenState();
}

class _TermsOfServiceScreenState extends State<TermsOfServiceScreen> {
  late WebViewController _controller;

  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
      ..loadRequest(Uri.parse('https://mia-cat.com/terms-of-service/'));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('利用規約')),
      body: WebViewWidget(controller: _controller),
    );
  }
}

無事、利用規約が表示された。AppBar左の「<:戻る」ボタンをクリックすると、設定画面に戻ることができる。

利用規約・プラポリのリンクをタップした時にWebView表示

今回は、navigation.pushによる画面遷移ではなく、下からスライドアップしてくるボトムシート表示方式にする。showModalBottomSheet関数を使用する。

showModalBottomSheet function - material library - Dart API
API docs for the showModalBottomSheet function from the material library, for the Dart programming language.

isScrollControlled: true:

  • このオプションを true に設定すると、ボトムシートが全画面モードで表示されることを許可する。これにより、ボトムシートの高さを自由に設定できるようになる。通常、ボトムシートは画面の半分の高さまでしか表示されないが、このオプションにより、例えば画面の80%の高さまで拡張することが可能(heightFactorで指定)。もちろん100%も可能。

FractionallySizedBox:

  • このウィジェットは、その子ウィジェットのサイズを親のサイズの割合で定義するために使用される。ここでは heightFactor0.8 に設定しており、これにより TermsOfServiceScreen の高さが親の高さ(つまり画面の高さ)の80%に設定される。
Dart
Widget buildItem(String item, bool isLast) {
    final user = ref.watch(userProvider);
    return Container(
      decoration: BoxDecoration(
        
      ),
      child: Column(
        children: [
          ListTile(
            title: Row(
             
            ),
            trailing: const Icon(Icons.chevron_right),
            onTap: () async {
              switch (item) {
                
                case '利用規約':
                  showModalBottomSheet(
                    context: context,
                    isScrollControlled: true,
                    builder: (_) => const FractionallySizedBox(
                      heightFactor: 0.8,
                      child: TermsOfServiceScreen(),
                    ),
                  );
                  break;
              }
            },
          ),
        ],
      ),
    );
  }

利用規約の表示のnavigationを修正

デフォルトは、AppBarの左側に「<」アイコンがあり、こちらをクリックすると利用規約の画面が閉じて、詳細設定画面に戻る。

しかし、この挙動は少々わかりづらいため、『<』は削除し、代わりにAppBarの右側に「×(閉じる)」ボタンを表示して、こちらをクリックすると画面を閉じられるように変更する。

AppBarleadingContainer()何も表示しない空のコンテナ)を配置することで、デフォルトの戻るボタン(<アイコン)を上書きして非表示にする。

そして、AppBarの右側に関しては、AppBarウィジェットのactionsプロパティを使用しする。actionsプロパティはウィジェットのリストを取り、これらのウィジェットはアプリバーの右側に水平に並べられる。

icon: const Icon(Icons.close), でAppBarウィジェットの右側に「閉じる」ボタンを表示する

Dart
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('利用規約'),
      leading: Container(),
      actions: <Widget>[
        IconButton(
          icon: const Icon(Icons.close),
          onPressed: () {
            Navigator.of(context).pop();
          },
        ),
      ],
    ),
    body: WebViewWidget(controller: _controller),
  );
}

分かりやすくなった。

WebViewをスクロール可能にする。

今のままでは、利用規約は表示されるもののスクロールできないので、スクロールできるようにする。

先ほどのshowModalBottomSheet関数は、enableDragというパラメーターがあり、デフォルトではtrueになっている。

showModalBottomSheetenableDragtrue に設定されている場合、ユーザーはボトムシート全体を上下にドラッグして操作できる。このドラッグ操作が、WebView内でのスクロール操作と競合する可能性がある。つまり、ユーザーがWebViewをスクロールしようとしたとき、そのジェスチャがボトムシートのドラッグ操作として解釈され、結果としてWebViewのスクロールが正常に動作しない可能性がある。

というわけで、enableDragfalse に設定して、ボトムシートのドラッグ操作を無効にする。ただしこの場合、ユーザーはボトムシートをドラッグして閉じることができなくなるが、今回はモーダルのAppBarの右側に、閉じるボタンを用意したので、ドラッグによるモーダル非表示機能は不要。

Dart
Widget buildItem(String item, bool isLast) {
    final user = ref.watch(userProvider);
    return Container(
      decoration: BoxDecoration(
        
      ),
      child: Column(
        children: [
          ListTile(
            title: Row(
             
            ),
            trailing: const Icon(Icons.chevron_right),
            onTap: () async {
              switch (item) {
                
                case '利用規約':
                  showModalBottomSheet(
                    context: context,
                    isScrollControlled: true,
                    enableDrag: false,
                    builder: (_) => const FractionallySizedBox(
                      heightFactor: 0.8,
                      child: TermsOfServiceScreen(),
                    ),
                  );
                  break;
              }
            },
          ),
        ],
      ),
    );
  }

動作確認

無事、利用規約のListItemをタップすると、画面下部から利用規約のWebViewが表示され、スクロールすることができ、モーダルの右上の閉じるアイコンで、モーダルを閉じることができるようになった。

コメント

タイトルとURLをコピーしました