【Flutter】Webviewでガワアプリの画面を作る

# はじめに

アプリ内でWebページを表示させるWebview画面を作成します。
このようなWebview画面がメインになるアプリを「ガワアプリ」といいます。
低コストでアプリを作成することが目的の場合に選択されます。
すでにあるWebページの流用や多数のモバイルエンジニアの確保が不要になります。(ただネイティブアプリのようなリッチな動きはできない)

# パッケージを追加

Webview画面を作るために以下を追加します。

```dart
// ステートレスな画面で変数の変化によって画面の表示を変えるためや、初期化処理のため
flutter pub add hooks_riverpod

// Webviewパッケージ
flutter pub add webview_flutter

// 外部ブラウザを開くためのパッケージ
flutter pub add url_launcher
```

# 画面を作成

Statelessな画面でWebivew画面を表示させています。
webviewで使用するコントローラーの初期化が終わったら、Webivew画面を表示させます。

```dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:webview_flutter/webview_flutter.dart';

/// QiitaのWebView画面
class QiitaWebViewScreen extends HookWidget {
  // 表示するURL
  final String url = 'https://qiita.com';

  // ホワイトリストに登録するドメイン
  final whiteDomainList = ['qiita.com'];

  @override
  Widget build(BuildContext context) {
    final isLoadingPage = useState(false);
    final webViewController = useState<WebViewController?>(null);

    // 初期化処理
    useEffect(() {
      Future<void> initWebView() async {
        final controller =
            WebViewController()
              ..setJavaScriptMode(JavaScriptMode.unrestricted)
              ..setBackgroundColor(Colors.white)
              ..setNavigationDelegate(
                NavigationDelegate(
                  onPageStarted: (String url) {
                    isLoadingPage.value = true;
                  },
                  onPageFinished: (String url) {
                    isLoadingPage.value = false;
                  },
                  onWebResourceError: (WebResourceError error) {
                    isLoadingPage.value = false;
                  },
                  onNavigationRequest: (NavigationRequest request) async {
                    final uri = Uri.parse(request.url);
                    // ホワイトリストに登録されていないドメインは外部ブラウザで開く
                    if (whiteDomainList.contains(uri.host)) {
                      return NavigationDecision.navigate;
                    }
                    final isSuccess = await launchUrl(uri, mode: LaunchMode.externalApplication);
                    if (!isSuccess) {
                      // URLを開けなかった場合はトーストを表示
                      ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('URLを開けませんでした: $url')));
                    }
                    return NavigationDecision.prevent;
                  },
                ),
              )
              ..loadRequest(Uri.parse(url));
        webViewController.value = controller;
      }

      // WebViewを初期化
      initWebView();

      return null;
    }, []); // 初回のみ実行

    // コントローラーが初期化されていない場合はローディング中のインジケーターを表示
    final _webViewController = webViewController.value;
    if (_webViewController == null) return const Center(child: CircularProgressIndicator(color: Colors.greenAccent));

    return Scaffold(
      appBar: AppBar(
        title: const Text('Qiita'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () {
              // WebViewをリロードする
              _webViewController.reload();
            },
          ),
        ],
      ),
      body: Stack(
        children: [
          WebViewWidget(controller: _webViewController),
          if (isLoadingPage.value) const Center(child: CircularProgressIndicator(color: Colors.greenAccent)),
        ],
      ),
    );
  }
}
```

![スクリーンショット 2025-04-28 8.47.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868196/9036a55f-ba7d-4f18-86c9-9f810f67f008.png)


# おわりに

今回はWebview画面を実装しました。
画面のみの実装のためHookWidgetを使用して実装していますが、ViewModelも作成するためConsumerHookWidgetを使用するように変更していくことになります。