[Flutter] webview_flutter App 作成

2023/03/11に公開

0. 前提条件

  • VSCodeのinstall
  • XCodeのinstall
  • Flutterのinstall

0.1. Appの完成系

User Storyは以下の通りです。

  • Userは任意のURLをHOMEで書き込める。
  • HOMEのButtonを押すことで任意のURLのPageを閲覧できる。

実際に完成するAppが以下の画像の通りです。


参考にさせていただいた記事は以下の記事です。
https://terupro.net/flutter-webview-app/

0.2. PCの情報

Software:
    System Software Overview:
      System Version: macOS 13.2.1 (22D68)
      Kernel Version: Darwin 22.3.0

Hardware:
    Hardware Overview:
      Model Name: MacBook Pro
      Chip: Apple M2 Pro
      Memory: 32 GB

0.3. Flutterのversion

% flutter --version
Flutter 3.7.6 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 12cb4eb7a0 (7 days ago) • 2023-03-01 10:29:26 -0800
Engine • revision ada363ee93
Tools • Dart 2.19.3 • DevTools 2.20.1

0.4. 実行環境

Editor: VSCode
Simulator: XCode(iOS)

1. 事前準備

1.1. project setup

以下の記事を参考にしてflutter projectの作成を行います。
https://zenn.dev/kazutxt/books/flutter_practice_introduction/viewer/07_chapter1_helloworld

1.2. packageのinstall

packageのinstall手順は以下の記事を参考にしています。
https://qiita.com/akeome/items/0a6ebf3af402fdf62c79

pubspec.yaml
name: flutter_webview
description: A new Flutter project.

publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: '>=2.19.3 <3.0.0'

dependencies:
  adaptive_dialog: ^1.5.0
  flutter:
    sdk: flutter
  flutter_riverpod: ^1.0.3
  webview_flutter: ^3.0.1

dev_dependencies:
  flutter_lints: ^1.0.0
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

libraryのinstall中には以下のような結果が出力されます。

[flutter_webview] flutter pub get
Running "flutter pub get" in flutter_webview...
Resolving dependencies...
...
Changed 16 dependencies!
exit code 0

2. Coding

最終的な完成系は以下のGitHubに載せてあります。
https://github.com/hinatha/flutter_webview

lib directory内にあるmain.dartというDart fileがentry pointとなっています。
以降は、lib/内でdart fileを作成し、開発します。

2.1. Directory構成

最終的なDirectory構成はこのようになっています。

.
├── .gitignore
├── .metadata
├── README.md
├── analysis_options.yaml
├── android
│   ├── app
│   │   ├── build.gradle
│   │   └── src
│   ├── build.gradle
│   ├── gradle
│   │   └── wrapper
│   ├── gradle.properties
│   └── settings.gradle
├── ios
│   ├── Flutter
│   │   ├── AppFrameworkInfo.plist
│   │   ├── Debug.xcconfig
│   │   └── Release.xcconfig
│   ├── Podfile
│   ├── Podfile.lock
│   ├── Runner
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets
│   │   ├── Base.lproj
│   │   ├── Info.plist
│   │   └── Runner-Bridging-Header.h
│   ├── Runner.xcodeproj
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace
│   │   └── xcshareddata
│   └── Runner.xcworkspace
│       ├── contents.xcworkspacedata
│       └── xcshareddata
├── lib
│   ├── main.dart
│   ├── provider.dart
│   └── view
│       ├── input_page.dart
│       └── web_page.dart
├── linux
│   ├── CMakeLists.txt
│   ├── flutter
│   │   ├── CMakeLists.txt
│   │   ├── generated_plugin_registrant.cc
│   │   ├── generated_plugin_registrant.h
│   │   └── generated_plugins.cmake
│   ├── main.cc
│   ├── my_application.cc
│   └── my_application.h
├── macos
│   ├── Flutter
│   │   ├── Flutter-Debug.xcconfig
│   │   ├── Flutter-Release.xcconfig
│   │   └── GeneratedPluginRegistrant.swift
│   ├── Podfile
│   ├── Runner
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets
│   │   ├── Base.lproj
│   │   ├── Configs
│   │   ├── DebugProfile.entitlements
│   │   ├── Info.plist
│   │   ├── MainFlutterWindow.swift
│   │   └── Release.entitlements
│   ├── Runner.xcodeproj
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace
│   │   └── xcshareddata
│   └── Runner.xcworkspace
│       ├── contents.xcworkspacedata
│       └── xcshareddata
├── pubspec.lock
├── pubspec.yaml
├── test
│   └── widget_test.dart
├── web
│   ├── favicon.png
│   ├── icons
│   │   ├── Icon-192.png
│   │   ├── Icon-512.png
│   │   ├── Icon-maskable-192.png
│   │   └── Icon-maskable-512.png
│   ├── index.html
│   └── manifest.json
└── windows
    ├── CMakeLists.txt
    ├── flutter
    │   ├── CMakeLists.txt
    │   ├── generated_plugin_registrant.cc
    │   ├── generated_plugin_registrant.h
    │   └── generated_plugins.cmake
    └── runner
        ├── CMakeLists.txt
        ├── Runner.rc
        ├── flutter_window.cpp
        ├── flutter_window.h
        ├── main.cpp
        ├── resource.h
        ├── resources
        ├── runner.exe.manifest
        ├── utils.cpp
        ├── utils.h
        ├── win32_window.cpp
        └── win32_window.h

37 directories, 67 files

2.2. Entry Point(main.dart)

main.dart
import 'package:flutter/material.dart'; // contains widgets implementing the Material Design guidelines
import 'package:flutter_riverpod/flutter_riverpod.dart'; // state management library for Flutter
import 'package:flutter_webview/view/input_page.dart'; // 'name in pubspec.yaml'/view/input_page.dart

// entry point of the application
void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  // It overrides the build() method to create the MaterialApp widget
  
  Widget build(BuildContext context) {
    // MaterialApp let us use material design
    return MaterialApp(
      // hides the debug banner in the top-right corner of the screen
      debugShowCheckedModeBanner: false,
      // the app title
      title: 'WebView',
      // Set default data in this app
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      // top page content
      home: const InputPage(),
    );
  }
}

2.3. Global stateの管理(provider.dart)

provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

// the initial value of the state is empty
final provider = StateProvider((ref) => '');
  • 管理しているState: Formで入力されるURL

Global stateの適用方法

main.dartで以下のようにProviderScope()をentry pointのargumentとして設定

main.dart
...
// entry point of the application
void main() {
  runApp(const ProviderScope(child: MyApp()));
}
...

2.4. Top page(input_page.dart)

input_page.dart
import 'package:adaptive_dialog/adaptive_dialog.dart'; // pre-built dialogs for different platforms
import 'package:flutter/material.dart'; // contains widgets implementing the Material Design guidelines
import 'package:flutter_riverpod/flutter_riverpod.dart'; // state management library for Flutter
import 'package:flutter_webview/provider.dart'; // 'name in pubspec.yaml'/provider.dart
import 'package:flutter_webview/view/web_page.dart'; // 'name in pubspec.yaml'/view/web_page.dart

class InputPage extends ConsumerWidget {
  const InputPage({Key? key}) : super(key: key);
  
  Widget build(BuildContext context, WidgetRef ref) {
    // manage text field widget
    final _controller = TextEditingController();
    // ref.watch: listen to changes in the state and rebuild the widget tree.
    // provider.notifier: access the notifier property of the provider object.
    final _provider = ref.watch(provider.notifier);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Input Page'),
        actions: [
          IconButton(
            onPressed: () {
              _controller.text = "";
            },
            icon: const Icon(Icons.autorenew),
          ),
        ],
      ),
      body: Padding(
        // Set padding in body
        padding: const EdgeInsets.all(20.0),
        child: Column(
          children: [
            TextField(
              controller: _controller,
              // change textfield state
              onChanged: (value) {
                _provider.state = value;
              },
            ),
            const SizedBox(height: 20.0),
            ElevatedButton(
              onPressed: () {
                if (_provider.state == "") {
                  // showOkAlertDialog is in adaptive_dialog package.
                  showOkAlertDialog(
                    // context provides the context of the current widget
                    context: context,
                    title: 'URLを入力してください',
                  );
                } else {
                  Navigator.push(context,
                      MaterialPageRoute(builder: (context) => const WebPage()));
                }
              },
              child: const Text('OPEN WEB'),
            ),
          ],
        ),
      ),
    );
  }
}

2.5. webview page(web_page.dart)

web_page.dart
import 'package:flutter/material.dart'; // contains widgets implementing the Material Design guidelines
import 'package:flutter_riverpod/flutter_riverpod.dart'; // state management library for Flutter
import 'package:flutter_webview/provider.dart'; // 'name in pubspec.yaml'/provider.dart
import 'package:webview_flutter/webview_flutter.dart'; // display web content within a Flutter app

class WebPage extends StatefulWidget {
  const WebPage({Key? key}) : super(key: key);
  
  // Manage web page state
  State<WebPage> createState() => _WebPageState();
}

class _WebPageState extends State<WebPage> {
  late WebViewController controller; // manage webview controller
  double progress = 0; // progress of loading the web page
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('WebPage'),
        actions: [
          IconButton(
            onPressed: () async {
              if (await controller.canGoBack()) {
                controller.goBack(); // go back to former page
              }
            },
            icon: const Icon(Icons.arrow_back),
          ),
          IconButton(
            onPressed: () {
              controller.reload(); // reload the web page
            },
            icon: const Icon(Icons.refresh),
          ),
        ],
      ),
      body: Column(
        children: [
          // Setting about progress bar
          LinearProgressIndicator(
            value: progress,
            color: Colors.red,
            backgroundColor: Colors.grey,
          ),
          Expanded(
            child: Consumer(
              builder: ((context, ref, child) {
                // Access provider to get the web url
                final _provider = ref.watch(provider.notifier);
                return WebView(
                  javascriptMode: JavascriptMode.unrestricted,
                  initialUrl: _provider.state, // the web url in provider state
                  onWebViewCreated: (controller) {
                    this.controller = controller; // Manage the web view
                  },
                  // When the web page is loading, showing a progress bar
                  // that is LinearProgressIndicator
                  onProgress: (progress) {
                    setState(() {
                      this.progress = progress / 100;
                    });
                  },
                );
              }),
            ),
          ),
        ],
      ),
    );
  }
}

3. Appの動作確認(iOS環境)

3.1. Simulatorの起動

iOS環境のSimulatorの起動は以下のCommandを実行します。

% open -a Simulator                             

3.2. Appの起動

Appの起動はterminal上で以下のcommandの実行によってできます。
今回はiOSのSimulatorで行なっているため、以下の表示になります。

% flutter run      
Launching lib/main.dart on iPhone 14 Pro Max in debug mode...
Running Xcode build...                                                  
 └─Compiling, linking and signing...                      2,712ms
Xcode build done.                                            5.7s
Syncing files to device iPhone 14 Pro Max...                        37ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on iPhone 14 Pro Max is available at: http://127.0.0.1:64681/bFsmeUts5gE=/
The Flutter DevTools debugger and profiler on iPhone 14 Pro Max is available at: http://127.0.0.1:9101?uri=http://127.0.0.1:64681/bFsmeUts5gE=/



これで動作確認が完了です。

Discussion