2022年3月13日日曜日

Flutter: Androidの場合、showDialog(AlertDialog)内でWebViewを使用するとボタンが見えなくなる現象の対処法

Androidの場合、showDialog(AlertDialog)内でWebViewを使用するとボタンが見えなくなり困っていたが対応できた。

Flutterバージョン
> flutter --version
Flutter 2.10.3 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 7e9793dee1 (10 days ago) • 2022-03-02 11:23:12 -0600
Engine • revision bd539267b4
Tools • Dart 2.16.1 • DevTools 2.9.2

対応前            →  対応後
 


使用しているWebView
webview_flutter: ^3.0.0
※執筆時最新バージョンは 3.0.1 

対処法
initState() に WebView.platform = AndroidWebView() を追加。
@override
void initState() {
  super.initState();

  // Enable virtual display.
  if (Platform.isAndroid) WebView.platform = AndroidWebView();
}

詳細
対応前は、ボタンは見えてないだけで、ボタンがあるあたりをタップするとちゃんとタップイベントが動作していたため、「見えてない」だけのようである。

また不思議なことに、次のページに遷移してそちらでもshowDialog()でダイアログ内にWebViewを使用しているのだが、こちらはちゃんとボタンが表示されていた。

なお、アプリ作成時(2019年3月ごろ)は、全画面ともダイアログのボタンは表示されていたため、webview_flutterをバージョン3.0.0に上げてからの症状だと思われる。

ネット上でもWebView関連の症状は結構あるようなので、その辺の問題であって、Flutterライブラリの更新はまだまだ目まぐるしい。
Flutterはその様に目まぐるしい状況なので、調査される方はあまり古い情報にハマらない様ご注意ください。(正攻法は「最新ライブラリのリファレンスを読む」ことなのだが、どうしても急いで対処療法を探すとネットに頼ってしまうこともあるかと思うため、念の為。)
(加えて言うならば、例えば.NET やJava だともう大きく変化しない箇所があって、「こういう時は、こう対応」みたいなものができていて、検索エンジンもそういったものを上位に持ってきてくれるので、古い情報でも気にせずに検索上位から当たっていく、みたいなやり方もアリといえばアリなのだろうが、マルチプラットフォーム開発の期待の星・または大本命であるFlutterはまだまだガンガン発展・進化を遂げていくであろうから、前者のやり方に慣れてしまった方は注意である。例えば.NETやJavaから、いきなりFlutter担当になった方など。「基本は本家リファレンスをあたる」という基本習慣が身について良いかも知れない。)

ちなみに、私のアプリもwebview_flutterをバージョン3.0.0にした時に、描画されるHTMLが全体的に小さくなったため、HTML側で以下を追加して対応してた。
<meta name="viewport" content="width=device-width, initial-scale=1.0">

webview_flutterバージョン3.0.0の変更履歴を見ると、BREAKING CHANGE(破壊的変更)とあって、ちょうどこの箇所が関係していたようだ。
BREAKING CHANGE: On Android, hybrid composition (SurfaceAndroidWebView) is 
now the default. The previous default, virtual display, can be specified with 
WebView.platform = AndroidWebView()
(出典: pub.dev webview_flutter)
それにしてもSurfaceAndroidWebViewがデフォルトになることで、なぜWebViewがダイアログのTextButtonに影響しているのかは不思議である。(私はこの症状にはなったことはないが、ネット上に「showDialogでWebViewを使うとWebViewがダイアログをはみ出す」とかもあったので、やはりWeb画面は色々できるので強力であり特殊なのだろう。)

※そもそもアプリの更新を半年くらいしてなかったら、DartがNull Safety正式版になってたりとか、ライブラリもWebView含め、Firebaseとか結構な変更があったので諸々対応して大変だった^^
 みなさんも放置しすぎない様に注意しましょう!(バージョンアップ対応に追われて、なかなかやりたかった更新作業に辿り着けない。。。)


(おまけ) コード抜粋(buildの骨組みだけ)
※Null Safetyは突貫で直しているので、_doShowDialog の String? contentStr は、String contentStrでいいんじゃないか、とかはご愛嬌^^
①問題が発生した画面
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(_appTitle),
          actions: <Widget>[
            PopupMenuButton(
              itemBuilder: (BuildContext context) {
                return [
                  PopupMenuItem(child: Text(AppLocalizations.of(context).aboutXxxTitle), value: 1),
                  PopupMenuItem(child: Text(AppLocalizations.of(context).aboutYyyTitle), value: 2),
                ];
              },
              onSelected: (menuId) {
                // ダイアログ表示
                if (menuId == 1) {
                  _doShowDialog(AppLocalizations.of(context).aboutXxxTitle, AppLocalizations.of(context).aboutXxx);
                } else if (menuId == 2) {
                  _doShowDialog(AppLocalizations.of(context).aboutYyyTitle, AppLocalizations.of(context).aboutYyy);
                }
              },
            ),
          ],
        ), // AppBar
        body: SafeArea(
          ...(省略)

  // ダイアログ表示
  _doShowDialog(String title, String? contentStr) {
    showDialog(
      context: context,
      builder: (_) {
        return AlertDialog(
          title: Text(title),
          content: Container(
            width: MediaQuery.of(context).size.width - 10,
            height: MediaQuery.of(context).size.height - 350,
            // 以下のWebViewは実際は別クラスにしてます。リソース(assets)テキストを表示するだけのWebView。
            child: WebView(
              onWebViewCreated: (WebViewController webViewController) async {
                // return the uri data from raw html string.
                String htmlString = '<!DOCTYPE html><body>' + contentStr! + '</body></html>';
                await webViewController.loadUrl(Uri.dataFromString(htmlString, encoding: Encoding.getByName('utf-8')).toString());
              },
            ),
          ),
          actions: <Widget>[
            // ↓これが見えなくなったボタン
            TextButton(
              child: Text("OK"),
              onPressed: () => Navigator.of(context, rootNavigator: true).pop(context), // これでダイアログが閉じられる
            ),
          ],
        );
      }
    );
  }

②問題が発生しなかった画面
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Expanded(
            child: StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
              // ページデータを読み込む
              stream: _pageData,
              builder: (BuildContext contextPageData,
                AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>> snapshot) {
                  if (snapshot.hasError) {
                    return Center(child: Text('Error: ${snapshot.error}'));
                  } else if (snapshot.connectionState == ConnectionState.none ||
                    snapshot.connectionState == ConnectionState.waiting) {
                    // ロード中
                    return _getLoadingText();
                  } else if (!snapshot.hasData) {
                    return _getLoadingText();
                  }

                  switch (snapshot.connectionState) {
                  case ConnectionState.waiting:
                    return _getLoadingText();
                  default:
                    // ページ情報
                    List<DocumentSnapshot<Map<String, dynamic>>> pages = snapshot.data!.docs;
                    return DefaultTabController(
                      length: pages.length,
                      initialIndex: _currPage!,
                      child: StatefulBuilder(
                        builder: (BuildContext contextTab, setState) =>
                          WillPopScope(
                              onWillPop: () {
                                // 前画面へ戻る
                                Navigator.of(context).pop();
                                return Future.value(false);
                              },
                              child: Scaffold(
                                appBar: AppBar(
                                  title: Text(widget.textDoc.data()!["title"]),
                                  actions: <Widget>[
                                    IconButton(
                                      icon: const Icon(Icons.info, color: Colors.white, size: 30,),
                                      onPressed: () {
                                        // 説明をダイアログ表示
                                        _doShowDialog(widget.textDoc.data()!["title"], widget.textDoc.data()!["text_info"]);
                                      },
                                    ),
                                  ],
                                ),

おまけなので、アプリの詳細は割愛するが、
①問題が発生した画面 では、appBarまたは、bodyの情報ボタンからダイアログを表示している。
②問題が発生しなかった画面 では、appBarからダイアログを表示している。
やっていることは、_pageDataとして外部からページデータを取得して、StreamBuilderでタブに表示している。そこそこありがちな構成だと思う。

構造としては、①だと Scaffoldだけだが、
②だと SafeArea → Expanded  DefaultTabController  StatefulBuilder  WillPopScope  Scaffold
となっており、この辺の構造も関係しているのだろうか?
ネット上にもScaffoldがどうのこうとと書いてあった様な。。。

0 件のコメント:

コメントを投稿