Flutter 앱에 WebView 추가

1. 소개

최종 업데이트: 2021년 10월 19일

WebView Flutter 플러그인을 사용하면 Android 또는 iOS Flutter 앱에 WebView 위젯을 추가할 수 있습니다. iOS에서는 WebView 위젯이 WKWebView로 지원되고, Android에서는 WebView 위젯이 WebView로 지원됩니다. 플러그인은 웹 뷰를 통해 Flutter 위젯을 렌더링할 수 있습니다. 예를 들어 웹 뷰를 통해 드롭다운 메뉴를 렌더링할 수 있습니다.

빌드할 항목

이 Codelab에서는 Flutter SDK를 사용하여 WebView 기능을 사용하는 모바일 앱을 단계별로 빌드합니다. 이 앱에는 아래의 기능이 있습니다.

  • WebView에 웹 콘텐츠 표시
  • WebView를 통해 스택된 Flutter 위젯 표시
  • 페이지 로드 진행률 이벤트에 반응
  • WebViewController를 통해 WebView를 제어
  • NavigationDelegate를 사용하여 웹사이트 차단
  • 자바스크립트 표현식 평가
  • JavascriptChannels를 사용하여 자바스크립트에서 콜백 처리
  • 쿠키 설정, 삭제, 추가, 표시
  • HTML이 포함된 애셋, 파일 또는 문자열에서 HTML 로드 및 표시

학습할 내용

이 Codelab에서는 다음을 비롯한 다양한 방식으로 webview_flutter 플러그인을 사용하는 방법을 알아봅니다.

  • webview_flutter 플러그인을 구성하는 방법
  • 페이지 로드 진행률 이벤트를 수신하는 방법
  • 페이지 탐색 제어 방법
  • WebView를 실행하여 기록의 앞뒤로 이동하는 방법
  • 반환된 결과 등을 사용하여 자바스크립트를 평가하는 방법
  • 자바스크립트에서 Dart 코드를 호출하도록 콜백을 등록하는 방법
  • 쿠키 관리 방법
  • 애셋이나 파일 또는 HTML이 포함된 문자열에서 HTML 페이지를 로드하고 표시하는 방법

필요한 항목

2. Flutter 환경 설정

이 실습을 완료하려면 Flutter SDK편집기라는 두 가지 소프트웨어가 필요합니다.

다음 기기 중 하나를 사용하여 이 Codelab을 실행할 수 있습니다.

  • 컴퓨터에 연결되어 있으며 개발자 모드로 설정된 실제 모바일 기기(Android 또는 iOS)
  • iOS 시뮬레이터 (macOS만 해당하며 Xcode 도구 설치 필요)
  • Android Emulator Android 스튜디오에서 설정 필요

3. 시작하기

Flutter 시작하기

Android 스튜디오Visual Studio Code 모두 이 작업을 위한 도구를 제공하며, 새로운 Flutter 프로젝트를 만드는 방법에는 여러 가지가 있습니다. 링크된 절차에 따라 프로젝트를 만들거나 편리한 명령줄 터미널에서 다음 명령어를 실행하세요.

$ flutter create webview_in_flutter
Creating project webview_in_flutter...
[Listing of created files elided]
Wrote 81 files.

All done!
In order to run your application, type:

  $ cd webview_in_flutter
  $ flutter run

Your application code is in webview_in_flutter\lib\main.dart.

WebView Flutter 플러그인을 종속 항목으로 추가

Pub 패키지를 사용하면 Flutter 앱에 기능을 더 쉽게 추가할 수 있습니다. 이 Codelab에서는 프로젝트에 webview_flutter 플러그인을 추가합니다. 터미널에서 다음 명령어를 실행합니다.

$ cd webview_in_flutter
$ flutter pub add webview_flutter
Resolving dependencies...
  async 2.8.1 (2.8.2 available)
  characters 1.1.0 (1.2.0 available)
  matcher 0.12.10 (0.12.11 available)
+ plugin_platform_interface 2.0.2
  test_api 0.4.2 (0.4.8 available)
  vector_math 2.1.0 (2.1.1 available)
+ webview_flutter 3.0.0
+ webview_flutter_android 2.8.0
+ webview_flutter_platform_interface 1.8.0
+ webview_flutter_wkwebview 2.7.0
Downloading webview_flutter 3.0.0...
Downloading webview_flutter_wkwebview 2.7.0...
Downloading webview_flutter_android 2.8.0...
Changed 5 dependencies!

pubspec.yaml을 검사하면 webview_flutter 플러그인의 종속 항목 섹션에 줄이 표시됩니다.

Android minSDK 구성

Android에서 webview_flutter 플러그인을 사용하려면 사용하려는 Android 플랫폼 뷰에 따라 minSDK를 19 또는 20으로 설정해야 합니다. Android 플랫폼 뷰에 관한 자세한 내용은 webview_flutter 플러그인 페이지에서 확인할 수 있습니다. 다음과 같이 android/app/build.gradle 파일을 수정합니다.

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
}

4. Flutter 앱에 WebView 위젯 추가

이 단계에서는 WebView를 애플리케이션에 추가합니다. WebView는 네이티브 뷰를 호스팅하며, 앱 개발자는 앱에서 이러한 네이티브 뷰를 호스팅하는 방법을 선택할 수 있습니다. Android에서는 가상 디스플레이(현재 Android의 기본)와 하이브리드 컴포지션 중에서 선택할 수 있습니다. 그러나 iOS는 항상 하이브리드 컴포지션을 사용합니다.

가상 디스플레이와 하이브리드 컴포지션의 차이점에 관한 자세한 내용은 플랫폼 뷰를 사용하여 Flutter 앱에서 네이티브 Android 및 iOS 뷰 호스팅에 관한 문서를 참조하세요.

화면에 WebView 표시

lib/main.dart의 내용을 다음으로 바꿉니다.

lib/main.dart

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

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

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
      ),
      body: const WebView(
        initialUrl: 'https://flutter.dev',
      ),
    );
  }
}

iOS 또는 Android에서 실행하면 WebView가 기기의 풀 블리드 브라우저 창으로 표시됩니다. 즉, 브라우저에서 테두리나 여백 없이 전체 화면으로 브라우저가 표시됩니다. 스크롤하면 약간 이상해 보이는 페이지 부분을 확인할 수 있습니다. 이는 현재 자바스크립트가 사용 중지되었고 flutter.dev를 렌더링하려면 자바스크립트가 필요하기 때문입니다.

하이브리드 컴포지션 사용 설정

Android 기기에 하이브리드 컴포지션 모드를 사용하려는 경우에는 몇 가지 간단한 수정을 하면 됩니다. 다음과 같이 lib/main.dart를 수정합니다.

lib/main.dart

import 'dart:io';                            // Add this import.
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

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

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  // Add from here ...
  @override
  void initState() {
    if (Platform.isAndroid) {
      WebView.platform = SurfaceAndroidWebView();
    }
    super.initState();
  }
  // ... to here.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
      ),
      body: const WebView(
        initialUrl: 'https://flutter.dev',
      ),
    );
  }
}

하이브리드 컴포지션 플랫폼 뷰를 사용하려면 build.gradleminSdkVersion을 19로 변경해야 합니다.

앱 실행

iOS 또는 Android에서 Flutter 앱을 실행하여 flutter.dev 웹사이트를 표시하는 WebView를 확인합니다. 또는 Android Emulator나 iOS 시뮬레이터에서 앱을 실행합니다. 첫 번째 WebView URL을 내 웹사이트 등으로 대체할 수 있습니다.

$ flutter run

적절한 시뮬레이터 또는 에뮬레이터가 실행되거나 실제 기기가 연결되어 있다면 앱을 컴파일하고 배포한 후 다음과 같은 내용이 표시됩니다.

5. 페이지 로드 이벤트 수신 대기

WebView 위젯은 앱이 수신할 수 있는 여러 페이지 로드 진행률 이벤트를 제공합니다. WebView 페이지 로드 주기 동안 onPageStarted, onProgress, onPageFinished의 세 가지 페이지 로드 이벤트가 발생합니다. 이 단계에서는 페이지 로드 표시기를 구현합니다. 보너스로, WebView 콘텐츠 영역을 통해 Flutter 콘텐츠를 렌더링할 수 있음을 보여줍니다.

앱에 페이지 로드 이벤트 추가

lib/src/web_view_stack.dart에서 새 소스 파일을 만들고 다음 콘텐츠를 입력합니다.

lib/src/web_view_stack.dart

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

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

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

이 코드는 WebView 위젯을 Stack에 래핑하여 페이지 로드 비율이 100% 미만일 때 조건부로 WebViewLinearProgressIndicator로 오버레이합니다. 이는 시간이 지남에 따라 변화하는 프로그램 상태를 포함하므로 StatefulWidget과 연결된 State 클래스에 이 상태를 저장했습니다.

이 새로운 WebViewStack 위젯을 활용하려면 다음과 같이 lib/main.dart를 수정하세요.

import 'package:flutter/material.dart';
// Delete the package:webview_flutter/webview_flutter.dart import
import 'src/web_view_stack.dart';  // Add this import

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

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

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
      ),
      body: const WebViewStack(),   // Replace the WebView widget with WebViewStack
    );
  }
}

앱을 실행할 때 네트워크 조건과 브라우저가 이동 중인 페이지를 캐시했는지에 따라 페이지 로드 표시기가 WebView 콘텐츠 영역 상단에 오버레이됩니다.

6. WebViewController를 사용하여 작업하기

WebView 위젯에서 WebViewController 액세스

WebView 위젯은 WebViewController를 통해 프로그래매틱 컨트롤을 사용합니다. 이 컨트롤러는 콜백을 통해 WebView 위젯을 구성한 후 사용할 수 있습니다. 이 컨트롤러는 비동기식으로 제공되므로 Dart의 비동기 Completer<T> 클래스의 주요 후보입니다.

다음과 같이 lib/src/web_view_stack.dart를 업데이트합니다.

lib/src/web_view_stack.dart

import 'dart:async';     // Add this import for Completer
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewStack extends StatefulWidget {
  const WebViewStack({required this.controller, Key? key}) : super(key: key); // Modify

  final Completer<WebViewController> controller;   // Add this attribute

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          // Add from here ...
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          // ... to here.
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

이제 WebViewStack 위젯이 Completer<WebViewController>를 사용하여 비동기식으로 생성된 컨트롤러를 게시합니다. 이는 앱의 나머지 부분에 컨트롤러를 제공하는 콜백 함수 인수를 만드는 것보다 더 가벼운 대안입니다.

탐색 컨트롤 만들기

WebView가 작동하는 것도 한 방법이지만 페이지 기록을 통해 앞뒤로 이동하고 페이지를 새로고침하는 것도 도움이 될 수 있습니다. 다행히 WebViewController를 사용하면 앱에 이 기능을 추가할 수 있습니다.

lib/src/navigation_controls.dart에서 새 소스 파일을 만들고 다음을 입력합니다.

lib/src/navigation_controls.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class NavigationControls extends StatelessWidget {
  const NavigationControls({required this.controller, Key? key})
      : super(key: key);

  final Completer<WebViewController> controller;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: controller.future,
      builder: (context, snapshot) {
        final WebViewController? controller = snapshot.data;
        if (snapshot.connectionState != ConnectionState.done ||
            controller == null) {
          return Row(
            children: const <Widget>[
              Icon(Icons.arrow_back_ios),
              Icon(Icons.arrow_forward_ios),
              Icon(Icons.replay),
            ],
          );
        }

        return Row(
          children: <Widget>[
            IconButton(
              icon: const Icon(Icons.arrow_back_ios),
              onPressed: () async {
                if (await controller.canGoBack()) {
                  await controller.goBack();
                } else {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('No back history item')),
                  );
                  return;
                }
              },
            ),
            IconButton(
              icon: const Icon(Icons.arrow_forward_ios),
              onPressed: () async {
                if (await controller.canGoForward()) {
                  await controller.goForward();
                } else {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('No forward history item')),
                  );
                  return;
                }
              },
            ),
            IconButton(
              icon: const Icon(Icons.replay),
              onPressed: () {
                controller.reload();
              },
            ),
          ],
        );
      },
    );
  }
}

이 위젯은 FutureBuilder<T> 위젯을 사용하여 컨트롤러를 사용할 수 있게 되면 적절히 다시 페인팅합니다. 컨트롤러를 사용할 수 있을 때까지 기다리는 동안 아이콘 세 개의 행이 렌더링되지만 컨트롤러가 나타나면 controller를 사용하여 기능을 구현하는 onPressed 핸들러가 포함된 IconButtonRow로 바뀝니다.

AppBar에 탐색 컨트롤 추가

업데이트된 WebViewStack과 새로 만든 NavigationControls가 준비됐으니 이제 업데이트된 WebViewApp에 모두 합쳐야 합니다. 이전 과정에서는 Completer<T>를 사용하는 방법을 알아봤지만 실제로 생성한 위치는 아니었습니다. 이 앱의 위젯 트리 상단 근처에 WebViewApp이 있으면 이 수준에서 만드는 것이 좋습니다.

lib/main.dart 파일을 다음과 같이 업데이트합니다.

lib/main.dart

import 'dart:async';                                    // Add this import

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';  // Add this import back

import 'src/navigation_controls.dart';                  // Add this import
import 'src/web_view_stack.dart';

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

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

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  final controller = Completer<WebViewController>();    // Instantiate the controller

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
        // Add from here ...
        actions: [
          NavigationControls(controller: controller),
        ],
        // ... to here.
      ),
      body: WebViewStack(controller: controller),       // Add the controller argument
    );
  }
}

앱을 실행하면 다음과 같은 컨트롤이 있는 웹페이지가 표시됩니다.

7. NavigationDelegate로 탐색 추적

WebView는 앱에서 WebView 위젯의 페이지 탐색을 추적하고 제어할 수 있도록 하는 NavigationDelegate,를 앱에 제공합니다. 탐색이 WebView,에서 시작될 때(예: 사용자가 링크를 클릭할 때) NavigationDelegate가 호출됩니다. NavigationDelegate 콜백은 WebView를 탐색으로 진행할지 제어하는 데 사용할 수 있습니다.

커스텀 NavigationDelegate 등록

이 단계에서는 NavigationDelegate 콜백을 등록하여 YouTube.com으로 이동을 차단합니다. 참고로 이러한 간단한 구현은 다양한 Flutter API 문서 페이지에 표시되는 인라인 YouTube 콘텐츠도 차단합니다.

다음과 같이 lib/src/web_view_stack.dart를 업데이트합니다.

lib/src/web_view_stack.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewStack extends StatefulWidget {
  const WebViewStack({required this.controller, Key? key}) : super(key: key);

  final Completer<WebViewController> controller;

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          // Add from here ...
          navigationDelegate: (navigation) {
            final host = Uri.parse(navigation.url).host;
            if (host.contains('youtube.com')) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text(
                    'Blocking navigation to $host',
                  ),
                ),
              );
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
          // ... to here.
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

다음 단계에서는 WebViewController 클래스를 사용하여 NavigationDelegate 테스트를 사용 설정하는 메뉴 항목을 추가합니다. 콜백의 로직을 강화하여 YouTube.com으로의 전체 페이지 탐색만 차단하고 API 문서에 인라인 YouTube 콘텐츠를 허용하는 것은 독자에게 연습으로 남겨 둡니다.

8. AppBar에 메뉴 버튼 추가

다음 몇 단계를 통해 AppBar 위젯에서 자바스크립트 평가, 자바스크립트 채널 호출, 쿠키 관리에 사용되는 메뉴 버튼을 만들어 보겠습니다. 물론 유용한 메뉴가 많습니다.

lib/src/menu.dart에서 새 소스 파일을 만들고 다음을 입력합니다.

lib/src/menu.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

enum _MenuOptions {
  navigationDelegate,
}

class Menu extends StatelessWidget {
  const Menu({required this.controller, Key? key}) : super(key: key);

  final Completer<WebViewController> controller;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                controller.data!.loadUrl('https://youtube.com');
                break;
            }
          },
          itemBuilder: (context) => [
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.navigationDelegate,
              child: Text('Navigate to YouTube'),
            ),
          ],
        );
      },
    );
  }
}

사용자가 YouTube로 이동 메뉴 옵션을 선택하면 WebViewControllerloadUrl 메서드가 실행됩니다. 이 탐색은 이전 단계에서 만든 navigationDelegate 콜백에 의해 차단됩니다.

WebViewApp의 화면에 메뉴를 추가하려면 다음과 같이 lib/main.dart를 수정합니다.

lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

import 'src/menu.dart';                                // Add this import
import 'src/navigation_controls.dart';
import 'src/web_view_stack.dart';

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

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

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  final controller = Completer<WebViewController>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
        actions: [
          NavigationControls(controller: controller),
          Menu(controller: controller),                // Add this line
        ],
      ),
      body: WebViewStack(controller: controller),
    );
  }
}

앱을 실행하고 YouTube로 이동 메뉴 항목을 탭합니다. 탐색 컨트롤러가 YouTube로 이동을 차단했다는 내용의 스낵바가 표시됩니다.

9. 자바스크립트 평가

WebViewController는 현재 페이지의 컨텍스트에서 자바스크립트 표현식을 평가할 수 있습니다. 자바스크립트를 평가하는 방법에는 두 가지가 있습니다. 값을 반환하지 않는 자바스크립트 코드의 경우 runJavaScript를 사용하고 값을 반환하는 자바스크립트 코드의 경우 runJavaScriptReturningResult를 사용합니다.

자바스크립트를 사용 설정하려면 javaScriptMode 속성이 JavascriptMode.unrestricted로 설정된 WebView 위젯을 구성해야 합니다. 기본적으로 javascriptModeJavascriptMode.disabled로 설정됩니다.

다음과 같이 javascriptMode 설정을 추가하여 _WebViewStackState 클래스를 업데이트합니다.

lib/src/web_view_stack.dart

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          navigationDelegate: (navigation) {
            final host = Uri.parse(navigation.url).host;
            if (host.contains('youtube.com')) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text(
                    'Blocking navigation to $host',
                  ),
                ),
              );
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
          javascriptMode: JavascriptMode.unrestricted,        // Add this line
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

이제 WebView가 자바스크립트를 실행할 수 있으므로 runJavaScriptReturningResult 메서드를 사용하는 옵션을 메뉴에 추가할 수 있습니다.

다음과 같이 lib/src/menu.dart를 수정합니다.

lib/src/menu.dart

enum _MenuOptions {
  navigationDelegate,
  userAgent,                                          // Add this line
}

class Menu extends StatelessWidget {
  const Menu({required this.controller, Key? key}) : super(key: key);

  final Completer<WebViewController> controller;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                controller.data!.loadUrl('https://youtube.com');
                break;
              // Add from here ...
              case _MenuOptions.userAgent:
                final userAgent = await controller.data!
                    .runJavascriptReturningResult('navigator.userAgent');
                ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(userAgent),
                ));
                break;
              // ... to here.
            }
          },
          itemBuilder: (context) => [
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.navigationDelegate,
              child: Text('Navigate to YouTube'),
            ),
            // Add from here ...
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.userAgent,
              child: Text('Show user-agent'),
            ),
            // ... to here.
          ],
        );
      },
    );
  }
}

'user-agent 표시' 메뉴 옵션을 탭하면 자바스크립트 표현식 navigator.userAgent의 실행 결과가 Snackbar에 표시됩니다. 앱을 실행하면 Flutter.dev 페이지가 다르게 표시됩니다. 이는 자바스크립트를 사용 설정한 상태로 실행한 결과입니다.

10. 자바스크립트 채널을 사용하여 작업하기

JavascriptChannel을 사용하면 앱이 WebView의 자바스크립트 컨텍스트에서 앱의 Dart 코드에 다시 값을 전달하도록 호출할 수 있는 콜백 핸들러를 등록할 수 있습니다. 이 단계에서는 XMLHttpRequest의 결과와 함께 호출되는 SnackBar 채널을 등록합니다.

다음과 같이 WebViewStack 클래스를 업데이트합니다.

lib/src/web_view_stack.dart

class WebViewStack extends StatefulWidget {
  const WebViewStack({required this.controller, Key? key}) : super(key: key);

  final Completer<WebViewController> controller;

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          navigationDelegate: (navigation) {
            final host = Uri.parse(navigation.url).host;
            if (host.contains('youtube.com')) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text(
                    'Blocking navigation to $host',
                  ),
                ),
              );
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
          javascriptMode: JavascriptMode.unrestricted,
          javascriptChannels: _createJavascriptChannels(context),  // Add this line
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }

  // Add from here ...
  Set<JavascriptChannel> _createJavascriptChannels(BuildContext context) {
    return {
      JavascriptChannel(
        name: 'SnackBar',
        onMessageReceived: (message) {
          ScaffoldMessenger.of(context)
              .showSnackBar(SnackBar(content: Text(message.message)));
        },
      ),
    };
  }
  // ... to here.
}

Set의 각 JavascriptChannel의 경우 자바스크립트 컨텍스트에서 채널 객체가 JavascriptChannel.name과 동일한 이름의 창 속성으로 제공됩니다. 자바스크립트 컨텍스트에서 이를 사용하려면 JavaScriptChannel에서 postMessage를 호출하여 이름이 지정된 JavascriptChannelonMessageReceived 콜백 핸들러에 전달되는 메시지를 전송해야 합니다.

위에 추가된 JavascriptChannel을 사용하려면 자바스크립트 컨텍스트에서 XMLHttpRequest를 실행하고 SnackBar JavascriptChannel을 사용하여 결과를 전달하는 다른 메뉴 항목을 추가합니다.

이제 WebViewJavascriptChannels,에 관해 알게 되었으므로 앱을 더 확장하는 예를 추가합니다. 이렇게 하려면 Menu 클래스에 PopupMenuItem 및 추가 기능을 추가하세요.

다음과 같이 javascriptChannel 열거형 값을 추가하여 추가 메뉴 옵션으로 _MenuOptions를 업데이트하고 구현을 Menu 클래스에 추가합니다.

lib/src/menu.dart

enum _MenuOptions {
  navigationDelegate,
  userAgent,
  javascriptChannel,                                    // Add this line
}

class Menu extends StatelessWidget {
  const Menu({required this.controller, Key? key}) : super(key: key);

  final Completer<WebViewController> controller;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                controller.data!.loadUrl('https://youtube.com');
                break;
              case _MenuOptions.userAgent:
                final userAgent = await controller.data!
                    .runJavascriptReturningResult('navigator.userAgent');
                ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(userAgent),
                ));
                break;
              // Add from here ...
              case _MenuOptions.javascriptChannel:
                await controller.data!.runJavascript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
  if (req.status == 200) {
    let response = JSON.parse(req.responseText);
    SnackBar.postMessage("IP Address: " + response.ip);
  } else {
    SnackBar.postMessage("Error: " + req.status);
  }
}
req.send();''');
                break;
              // ... to here.
            }
          },
          itemBuilder: (context) => [
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.navigationDelegate,
              child: Text('Navigate to YouTube'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.userAgent,
              child: Text('Show user-agent'),
            ),
            // Add from here ...
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.javascriptChannel,
              child: Text('Lookup IP Address'),
            ),
            // ... to here.
          ],
        );
      },
    );
  }
}

이 자바스크립트는 사용자가 자바스크립트 채널 예 메뉴 옵션을 선택할 때 실행됩니다.

var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
  if (req.status == 200) {
    SnackBar.postMessage(req.responseText);
  } else {
    SnackBar.postMessage("Error: " + req.status);
  }
}
req.send();

이 코드는 공개 IP 주소 API에 GET 요청을 전송하여 기기의 IP 주소를 반환합니다. 이 결과는 SnackBar JavascriptChannel에서 postMessage를 호출하여 SnackBar에 표시됩니다.

11. 쿠키 관리

앱은 CookieManager 클래스를 사용하여 WebView의 쿠키를 관리할 수 있습니다. 이 단계에서는 쿠키 목록을 표시하고, 쿠키 목록을 삭제하고, 쿠키를 삭제하고, 새 쿠키를 설정합니다. 다음과 같이 각 쿠키 사용 사례의 항목을 _MenuOptions에 추가합니다.

lib/src/menu.dart

enum _MenuOptions {
  navigationDelegate,
  userAgent,
  javascriptChannel,
  // Add from here ...
  listCookies,
  clearCookies,
  addCookie,
  setCookie,
  removeCookie,
  // ... to here.
}

이 단계의 나머지 변경사항은 스테이트리스(Stateless)에서 스테이트풀(Stateful)로의 Menu 클래스 변환을 비롯하여 Menu 클래스에 중점을 둡니다. 이 변경사항은 MenuCookieManager를 소유해야 하고 스테이트리스(Stateless) 위젯의 변경 가능한 상태가 잘못된 조합이기 때문에 중요합니다.

편집기 또는 키보드 작업을 사용하여 메뉴 클래스를 StatefulWidget으로 변환하고 CookieManager를 결과 스테이트(State) 클래스에 다음과 같이 추가합니다.

lib/src/menu.dart

class Menu extends StatefulWidget {                           // Convert to StatefulWidget
  const Menu({required this.controller, Key? key}) : super(key: key);

  final Completer<WebViewController> controller;

  @override
  State<Menu> createState() => _MenuState();                  // Add this line
}

class _MenuState extends State<Menu> {                       // New State class
  final CookieManager cookieManager = CookieManager();       // Add this line

  @override
  Widget build(BuildContext context) {
  // ...

_MenuState 클래스에는 새로 추가된 CookieManager와 함께 이전에 Menu 클래스에 추가한 코드가 포함됩니다. 다음 섹션에서는 아직 추가되지 않은 메뉴 항목으로 인해 차례로 호출되는 도우미 함수를 _MenuState에 추가합니다.

모든 쿠키 목록 가져오기

자바스크립트를 사용하여 모든 쿠키 목록을 가져옵니다. 이렇게 하려면 _MenuState 클래스 끝에 _onListCookies라는 도우미 메서드를 추가합니다. runJavaScriptReturningResult 메서드를 사용하면 도우미 메서드는 자바스크립트 컨텍스트에서 document.cookie를 실행하여 모든 쿠키 목록을 반환합니다.

_MenuState 클래스에 다음을 추가합니다.

lib/src/menu.dart

Future<void> _onListCookies(WebViewController controller) async {
  final String cookies =
      await controller.runJavascriptReturningResult('document.cookie');
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(cookies.isNotEmpty ? cookies : 'There are no cookies.'),
    ),
  );
}

쿠키 모두 삭제

WebView에서 모든 쿠키를 삭제하려면 CookieManager 클래스의 clearCookies 메서드를 사용합니다. 이 메서드는 CookieManager가 쿠키를 삭제하면 true로 확인되고 삭제할 쿠키가 없는 경우 false으로 확인되는 Future<bool>을 반환합니다.

_MenuState 클래스에 다음을 추가합니다.

lib/src/menu.dart

Future<void> _onClearCookies() async {
  final hadCookies = await cookieManager.clearCookies();
  String message = 'There were cookies. Now, they are gone!';
  if (!hadCookies) {
    message = 'There were no cookies to clear.';
  }
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(message),
    ),
  );
}

자바스크립트를 호출하여 쿠키를 추가할 수 있습니다. 자바스크립트 문서에 쿠키를 추가하는 데 사용되는 API는 MDN에 자세히 설명되어 있습니다.

_MenuState 클래스에 다음을 추가합니다.

lib/src/menu.dart

Future<void> _onAddCookie(WebViewController controller) async {
  await controller.runJavascript('''var date = new Date();
  date.setTime(date.getTime()+(30*24*60*60*1000));
  document.cookie = "FirstName=John; expires=" + date.toGMTString();''');
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie added.'),
    ),
  );
}

다음과 같이 CookieManager를 사용하여 쿠키를 설정할 수도 있습니다.

_MenuState 클래스에 다음을 추가합니다.

lib/src/menu.dart

Future<void> _onSetCookie(WebViewController controller) async {
  await cookieManager.setCookie(
    const WebViewCookie(name: 'foo', value: 'bar', domain: 'flutter.dev'),
  );
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie is set.'),
    ),
  );
}

쿠키를 삭제하려면 만료일이 과거로 설정된 쿠키를 추가해야 합니다.

_MenuState 클래스에 다음을 추가합니다.

lib/src/menu.dart

Future<void> _onRemoveCookie(WebViewController controller) async {
  await controller.runJavascript(
      'document.cookie="FirstName=John; expires=Thu, 01 Jan 1970 00:00:00 UTC" ');
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie removed.'),
    ),
  );
}

CookieManager 메뉴 항목 추가

메뉴 옵션을 추가하고 방금 추가한 도우미 메서드에 연결하기만 하면 됩니다. 다음과 같이 _MenuState 클래스를 업데이트합니다.

lib/src/menu.dart

class _MenuState extends State<Menu> {
  final CookieManager cookieManager = CookieManager();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: widget.controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                controller.data!.loadUrl('https://youtube.com');
                break;
              case _MenuOptions.userAgent:
                final userAgent = await controller.data!
                    .runJavascriptReturningResult('navigator.userAgent');
                ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(userAgent),
                ));
                break;
              case _MenuOptions.javascriptChannel:
                await controller.data!.runJavascript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
  if (req.status == 200) {
    let response = JSON.parse(req.responseText);
    SnackBar.postMessage("IP Address: " + response.ip);
  } else {
    SnackBar.postMessage("Error: " + req.status);
  }
}
req.send();''');
                break;
              // Add from here ...
              case _MenuOptions.clearCookies:
                _onClearCookies();
                break;
              case _MenuOptions.listCookies:
                _onListCookies(controller.data!);
                break;
              case _MenuOptions.addCookie:
                _onAddCookie(controller.data!);
                break;
              case _MenuOptions.setCookie:
                _onSetCookie(controller.data!);
                break;
              case _MenuOptions.removeCookie:
                _onRemoveCookie(controller.data!);
                break;
              // ... to here.
            }
          },
          itemBuilder: (context) => [
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.navigationDelegate,
              child: Text('Navigate to YouTube'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.userAgent,
              child: Text('Show user-agent'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.javascriptChannel,
              child: Text('Lookup IP Address'),
            ),
            // Add from here ...
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.clearCookies,
              child: Text('Clear cookies'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.listCookies,
              child: Text('List cookies'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.addCookie,
              child: Text('Add cookie'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.setCookie,
              child: Text('Set cookie'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.removeCookie,
              child: Text('Remove cookie'),
            ),
            // ... to here.
          ],
        );
      },
    );
  }

CookieManager 실행

앱에 방금 추가한 모든 기능을 사용하려면 다음 단계를 따르세요.

  1. 쿠키 나열을 선택합니다. flutter.dev에서 설정한 Google 애널리틱스 쿠키가 목록에 표시됩니다.
  2. 쿠키 지우기를 선택합니다. 쿠키가 실제로 삭제되었다고 보고되어야 합니다.
  3. 쿠키 지우기를 다시 선택합니다. 그러면 삭제할 수 있는 쿠키가 없다고 보고됩니다.
  4. 쿠키 나열을 선택합니다. 쿠키가 없다고 보고되어야 합니다.
  5. 쿠키 추가를 선택합니다. 그러면 추가된 쿠키로 보고됩니다.
  6. 쿠키 설정을 선택합니다. 그러면 쿠키가 설정된 것으로 보고됩니다.
  7. 쿠키 나열을 선택한 다음 마지막으로 쿠키 삭제를 선택합니다.

12. WebView에서 Flutter 애셋, 파일, HTML 문자열 로드

앱은 다양한 메서드를 사용하여 HTML 파일을 로드하고 WebView에 표시할 수 있습니다. 이 단계에서는 pubspec.yaml 파일에 지정된 Flutter 애셋을 로드하고, 지정된 경로에 있는 파일을 로드하고, HTML 문자열을 사용하여 페이지를 로드합니다.

지정된 경로에 있는 파일을 로드하려면 path_providerpubspec.yaml에 추가해야 합니다. 이는 파일 시스템에서 흔히 사용되는 위치를 찾기 위한 Flutter 플러그인입니다.

pubspec.yaml에서 다음 줄을 추가합니다.

pubspec.yaml

dependencies:
 flutter:
   sdk: flutter

 # The following adds the Cupertino Icons font to your application.
 # Use with the CupertinoIcons class for iOS style icons.
 cupertino_icons: ^1.0.2
 webview_flutter: ^3.0.0
 path_provider: ^2.0.7   # Add this line

애셋을 로드하려면 pubspec.yaml에 애셋 경로를 지정해야 합니다. pubspec.yaml에서 다음 줄을 추가합니다.

pubspec.yaml

# The following section is specific to Flutter.
flutter:

 # The following line ensures that the Material Icons font is
 # included with your application, so that you can use the icons in
 # the material Icons class.
 uses-material-design: true
 # Add from here
 assets:
   - assets/www/index.html
   - assets/www/styles/style.css
 # ... to here.

프로젝트에 애셋을 추가하려면 다음 단계를 따릅니다.

  1. 프로젝트의 루트 폴더에 이름이 assets인 새 디렉터리를 만듭니다.
  2. assets 폴더에 이름이 www인 새 디렉터리를 만듭니다.
  3. www 폴더에 이름이 styles인 새 디렉터리를 만듭니다.
  4. www 폴더에 이름이 index.html인 새 파일을 만듭니다.
  5. styles 폴더에 이름이 style.css인 새 파일을 만듭니다.

다음 코드를 복사하여 index.html 파일에 붙여넣습니다.

assets/www/index.html

<!DOCTYPE html>
<!-- Copyright 2013 The Flutter Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<html lang="en">
<head>
<title>Load file or HTML string example</title>
<link rel="stylesheet" href="styles/style.css" />
</head>
<body>

<h1>Local demo page</h1>
<p>
 This is an example page used to demonstrate how to load a local file or HTML
 string using the <a href="https://pub.dev/packages/webview_flutter">Flutter
 webview</a> plugin.
</p>

</body>
</html>

style.css의 경우 다음 몇 줄을 사용하여 HTML 헤더 스타일을 설정합니다.

assets/www/styles/style.css

h1 {
   color: blue;
}

이제 애셋을 설정하고 사용할 준비가 되었으므로 Flutter 애셋, 파일 또는 HTML 문자열을 로드하고 표시하는 데 필요한 메서드를 구현할 수 있습니다.

Flutter 애셋 로드

방금 만든 애셋을 로드하려면 WebViewController를 사용하여 loadFlutterAsset 메서드를 호출하고 애셋 경로를 매개변수로 제공하면 됩니다. 코드 끝에 다음 메서드를 추가합니다.

lib/src/menu.dart

Future<void> _onLoadFlutterAssetExample(
   WebViewController controller, BuildContext context) async {
 await controller.loadFlutterAsset('assets/www/index.html');
}

로컬 파일 로드

기기에서 파일을 로드하는 경우 파일의 경로를 포함하는 String이 적용된 WebViewController를 사용하여 loadFile 메서드를 사용할 메서드를 다시 추가할 수 있습니다.

먼저 HTML 코드가 포함된 파일을 만들어야 합니다. 이렇게 하려면 가져오기 바로 아래에 있는 menu.dart 파일의 코드 상단에 HTML 코드를 문자열로 추가하면 됩니다.

lib/src/menu.dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:webview_flutter/webview_flutter.dart';

// Add from here ...
const String kExamplePage = '''
<!DOCTYPE html>
<html lang="en">
<head>
<title>Load file or HTML string example</title>
</head>
<body>

<h1>Local demo page</h1>
<p>
 This is an example page used to demonstrate how to load a local file or HTML
 string using the <a href="https://pub.dev/packages/webview_flutter">Flutter
 webview</a> plugin.
</p>

</body>
</html>
''';
// ... to here.

File을 만들고 HTML 문자열을 파일에 쓰려면 두 개의 메서드를 추가합니다. _onLoadLocalFileExample은 경로를 _prepareLocalFile() 메서드에서 반환하는 문자열로 제공하여 파일을 로드합니다. 코드에 다음 메서드를 추가합니다.

lib/src/menu.dart

Future<void> _onLoadFlutterAssetExample(
   WebViewController controller, BuildContext context) async {
 await controller.loadFlutterAsset('assets/www/index.html');
}

Future<void> _onLoadLocalFileExample(
   WebViewController controller, BuildContext context) async {
 final String pathToIndex = await _prepareLocalFile();

 await controller.loadFile(pathToIndex);
}

static Future<String> _prepareLocalFile() async {
 final String tmpDir = (await getTemporaryDirectory()).path;
 final File indexFile = File('$tmpDir/www/index.html');

 await Directory('$tmpDir/www').create(recursive: true);
 await indexFile.writeAsString(kExamplePage);

 return indexFile.path;
}
// ... to here.

HTML 문자열 로드

HTML 문자열을 제공하여 페이지를 표시하는 방법은 아주 간단합니다. WebViewController에는 HTML 문자열을 인수로 제공할 수 있는 loadHtmlString이라는 메서드가 있습니다. 그러면 WebView에 제공된 HTML 페이지가 표시됩니다. 코드에 다음 메서드를 추가합니다.

lib/src/menu.dart

Future<void> _onLoadFlutterAssetExample(
   WebViewController controller, BuildContext context) async {
 await controller.loadFlutterAsset('assets/www/index.html');
}

Future<void> _onLoadLocalFileExample(
   WebViewController controller, BuildContext context) async {
 final String pathToIndex = await _prepareLocalFile();

 await controller.loadFile(pathToIndex);
}

static Future<String> _prepareLocalFile() async {
 final String tmpDir = (await getTemporaryDirectory()).path;
 final File indexFile = File('$tmpDir/www/index.html');

 await Directory('$tmpDir/www').create(recursive: true);
 await indexFile.writeAsString(kExamplePage);

 return indexFile.path;
}

// Add here ...
Future<void> _onLoadHtmlStringExample(
   WebViewController controller, BuildContext context) async {
 await controller.loadHtmlString(kExamplePage);
}
// ... to here.

메뉴 항목 추가

이제 애셋을 설정하고 사용할 준비가 되었으며 모든 기능을 갖춘 메서드가 있으므로 메뉴를 업데이트할 수 있습니다. 다음 항목을 _MenuOptions enum에 추가합니다.

lib/src/menu.dart

enum _MenuOptions {
  navigationDelegate,
  userAgent,
  javascriptChannel,
  listCookies,
  clearCookies,
  addCookie,
  setCookie,
  removeCookie,
  // Add from here ...
  loadFlutterAsset,
  loadLocalFile,
  loadHtmlString,
  // ... to here.
}

enum이 업데이트되었으므로 메뉴 옵션을 추가하고 방금 추가한 도우미 메서드에 연결할 수 있습니다. 다음과 같이 _MenuState 클래스를 업데이트합니다.

lib/src/menu.dart

class _MenuState extends State<Menu> {
 final CookieManager cookieManager = CookieManager();

 @override
 Widget build(BuildContext context) {
   return FutureBuilder<WebViewController>(
     future: widget.controller.future,
     builder: (context, controller) {
       return PopupMenuButton<_MenuOptions>(
         onSelected: (value) async {
           switch (value) {
             case _MenuOptions.navigationDelegate:
               controller.data!.loadUrl('https://youtube.com');
               break;
             case _MenuOptions.userAgent:
               final userAgent = await controller.data!
                   .runJavascriptReturningResult('navigator.userAgent');
               ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                 content: Text(userAgent),
               ));
               break;
             case _MenuOptions.javascriptChannel:
               await controller.data!.runJavascript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
 if (req.status == 200) {
   let response = JSON.parse(req.responseText);
   SnackBar.postMessage("IP Address: " + response.ip);
 } else {
   SnackBar.postMessage("Error: " + req.status);
 }
}
req.send();''');
               break;
             case _MenuOptions.clearCookies:
               _onClearCookies();
               break;
             case _MenuOptions.listCookies:
               _onListCookies(controller.data!);
               break;
             case _MenuOptions.addCookie:
               _onAddCookie(controller.data!);
               break;
             case _MenuOptions.setCookie:
               _onSetCookie(controller.data!);
               break;
             case _MenuOptions.removeCookie:
               _onRemoveCookie(controller.data!);
               Break;
             // Add from here ...
             case _MenuOptions.loadFlutterAsset:
               _onLoadFlutterAssetExample(controller.data!, context);
               break;
             case _MenuOptions.loadLocalFile:
               _onLoadLocalFileExample(controller.data!, context);
               break;
             case _MenuOptions.loadHtmlString:
               _onLoadHtmlStringExample(controller.data!, context);
               Break;
             // ... to here.
           }
         },
         itemBuilder: (context) => [
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.navigationDelegate,
             child: Text('Navigate to YouTube'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.userAgent,
             child: Text('Show user-agent'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.javascriptChannel,
             child: Text('Lookup IP Address'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.clearCookies,
             child: Text('Clear cookies'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.listCookies,
             child: Text('List cookies'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.addCookie,
             child: Text('Add cookie'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.setCookie,
             child: Text('Set cookie'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.removeCookie,
             child: Text('Remove cookie'),
           ),
           // Add from here ...
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.loadFlutterAsset,
             child: Text('Load Flutter Asset'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.loadHtmlString,
             child: Text('Load HTML string'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.loadLocalFile,
             child: Text('Load local file'),
           ),
           // ... to here.
         ],
       );
     },
   );
 }

애셋, 파일, HTML 문자열 테스트

방금 구현한 코드가 작동하는지 테스트하려면 기기에서 코드를 실행하고 새로 추가된 메뉴 항목 중 하나를 클릭하면 됩니다. _onLoadFlutterAssetExample에서 HTML 파일의 헤더를 파란색으로 변경하기 위해 추가한 style.css를 어떻게 사용하는지 주목하세요.

13. 완료

수고하셨습니다. Codelab을 완료했습니다. 이 Codelab의 완료된 코드는 Codelab 저장소에서 확인할 수 있습니다.

자세한 내용은 다른 Flutter Codelab을 참고하세요.