Flutter Webview網頁與App通訊

Tecode發表於2021-08-08

前言

開發過程中我們會遇到App巢狀網頁的使用場景,我們在App中跳轉到H5網頁有時候網頁中會有跳轉到App的需求,也就是App與網頁的相互通訊。下面我們來慢慢的實現這個功能。

準備工作

安裝Flutter外掛webview_flutter

pubspec.yaml新增依賴

開發使用的Flutter版本是2.2.3,dart版本是2.13.x,安裝webview_flutter: ^2.0.10最低dart版本>=2.12.x,建議使用新的版本,之前有遇到安卓手機不能彈起輸入鍵盤、記憶體洩漏、文字不能複製等一系列問題,2.0.10這個版本沒有遇見類似問題。

dependencies:
...
 webview_flutter: ^2.0.10
..
複製程式碼

使用webview_flutter外掛

import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_nxj_c/helpers/colors.dart';
import 'package:flutter_nxj_c/helpers/config.dart';
import 'package:flutter_nxj_c/stores/hybrid_h5.dart';
import 'package:webview_flutter/webview_flutter.dart';

class HybridH5 extends StatefulWidget {
  static const String routeName = '/hybrid_h5';
  const HybridH5();
  @override
  _HybridH5State createState() => _HybridH5State();
}

class _HybridH5State extends State<HybridH5> {
  final Completer<WebViewController> _controller = Completer<WebViewController>();
  final _hybridH5Store = HybridH5Store();
  late WebViewController _webViewController;
  String title = '載入中...';

  @override
  void initState() {
    super.initState();
    if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
    Future(() async {
      // 請求引數引數
      var params = ModalRoute.of(context)!.settings.arguments;
      debugPrint('WebView引數-----$params');
      // Modal.loading(duration: Duration.zero);
      await _initController();
    });
  }

  Future<void> _initController() async {
    await _controller.future.then((controller) {
      _webViewController = controller;
      _webViewController.loadUrl('https://juejin.cn/user/1046390798028072');
    });
  }

  @override
  Widget build(BuildContext context) => WillPopScope(
        onWillPop: () async {
          var readyController = await _controller.future;
          if (await readyController.canGoBack()) {
            await readyController.goBack();
            return Future.value(false);
          }
          return Future.value(true);
        },
        child: CupertinoPageScaffold(
          backgroundColor: ColorTheme.of(context).colorF3F3F6,
          navigationBar: CupertinoNavigationBar(
            backgroundColor: ColorTheme.of(context).colorFFFFFF,
            padding: EdgeInsetsDirectional.zero,
            border: Border.all(color: ColorTheme.of(context).borderColor),
            transitionBetweenRoutes: Platform.isIOS,
            middle: Text('$title'),
            leading: GestureDetector(
              behavior: HitTestBehavior.opaque,
              onTap: () async {
                var readyController = await _controller.future;
                if (await readyController.canGoBack()) {
                  await readyController.goBack();
                  return;
                }
                Navigator.pop(context, '資料傳參');
              },
              child: Container(
                width: 42.0,
                padding: const EdgeInsets.only(left: 10.0, right: 20.0),
                child: Image.asset(
                  'assets/icons/ic_arrow_left_gray.png',
                  color: ColorTheme.of(context).color202326,
                ),
              ),
            ),
          ),
          child: WebView(
            javascriptMode: JavascriptMode.unrestricted,
            onWebViewCreated: (webViewController) {
              _controller.complete(webViewController);
            },
            onProgress: (progress) {
              print('WebView is loading (progress : $progress%)');
            },
            javascriptChannels: <JavascriptChannel>{
              _toasterJavascriptChannel(context),
            },
            navigationDelegate: (request) => _hybridH5Store.listenNavigationDelegate(request),
            onPageStarted: (url) {
              print('Page started loading: $url');
            },
            onPageFinished: (url) async {
              debugPrint('Page finished loading: $url');
              await _webViewController.evaluateJavascript('document.title').then((result) {
                debugPrint('標題--: $result');
                if (result.replaceAll('"', '').isNotEmpty) {
                  setState(() => title = result.replaceAll('"', ''));
                }
              });
            },
            gestureNavigationEnabled: true,
          ),
        ),
      );

  JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
    return JavascriptChannel(
        name: 'Toaster',
        onMessageReceived: (message) {
          // ignore: deprecated_member_use
          Scaffold.of(context).showSnackBar(
            SnackBar(content: Text(message.message)),
          );
        });
  }
}
複製程式碼

初始化載入地址,設定headers

使用_webViewController.loadUrl方法載入網址,這個地方我們可以headers,有token需求的同學們就可以直接將App內的token放到瀏覽器中,我們也可以訪問cookie

Future<void> _initController() async {
  await _controller.future.then((controller) {
    _webViewController = controller;
    _webViewController.loadUrl('https://juejin.cn/user/1046390798028072',headers:{});
  });
}
複製程式碼

Webview元件構建

webview生成成功以後呼叫_controller.complete(webViewController)將我們需要訪問的網址放入,瀏覽器就會訪問指定的網頁

WebView(
  javascriptMode: JavascriptMode.unrestricted,
  onWebViewCreated: (webViewController) {
    _controller.complete(webViewController);
  },
  onProgress: (progress) {
    print('WebView is loading (progress : $progress%)');
  },
  javascriptChannels: <JavascriptChannel>{
    _toasterJavascriptChannel(context),
  },
  navigationDelegate: (request) => _hybridH5Store.listenNavigationDelegate(request),
  onPageStarted: (url) {
    print('Page started loading: $url');
  },
  onPageFinished: (url) async {
    debugPrint('Page finished loading: $url');
    await _webViewController.evaluateJavascript('document.title').then((result) {
      debugPrint('標題--: $result');
      if (result.replaceAll('"', '').isNotEmpty) {
        setState(() => title = result.replaceAll('"', ''));
      }
    });
  },
  gestureNavigationEnabled: true,
)
複製程式碼

獲取瀏覽器訪問的頁面title

在瀏覽器訪問網頁的過程中我們會修改標題,達到不同頁面顯示不同標題的功能。

呼叫_webViewController.evaluateJavascript('document.title')方法獲取網頁標題。

注意:這是一個非同步方法。

onPageFinished: (url) async {
  debugPrint('Page finished loading: $url');
  await _webViewController.evaluateJavascript('document.title').then((result) {
    debugPrint('標題--: $result');
    if (result.replaceAll('"', '').isNotEmpty) {
      setState(() => title = result.replaceAll('"', ''));
    }
  });
}
複製程式碼

Screenshot_1628435985.png

Screenshot_1628435963.png

瀏覽器網頁與App的相互通訊

App如何接收到網頁的方法

WebView增加javascriptChannelsjavascriptChannelsjavaScript的管道可以包含很多個自己定義的方法。

javascriptChannels: <JavascriptChannel>{
// 彈出App內的提示框
  _toasterJavascriptChannel(context),
// 儲存圖片到相簿
  _fileDownLoaderChannel(),
// 新增自定義的方法處理網頁
  ...
},
複製程式碼

_toasterJavascriptChannel

JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
  return JavascriptChannel(
      name: 'Toaster',
      onMessageReceived: (message) {
        // ignore: deprecated_member_use
        Scaffold.of(context).showSnackBar(
          SnackBar(content: Text(message.message)),
        );
      });
}
複製程式碼

_fileDownLoaderChannel

JavascriptChannel _fileDownLoaderChannel() {
  return JavascriptChannel(
      name: 'fileDownLoader',
      onMessageReceived: (message) async {
        // 跳轉到指定頁面
        try {
          final data = json.decode(message.message) as Map<String, dynamic>;
          var response = await api.Interceptor.dio
              .get<Options>("${data['url']}", options: Options(responseType: ResponseType.bytes));
          await ImageGallerySaver.saveImage(Uint8List.fromList(response.data as List<int>));
          Toast.info(msg: '儲存圖片成功', showTime: 5000);
        } catch (err) {
          Toast.info(msg: '儲存圖片失敗', showTime: 5000);
        }
      });
}
複製程式碼

網頁通知App彈出提示框、儲存圖片到相簿

// 儲存圖片到App
window.fileDownLoader.postMessage(JSON.stringify({ url: detailData.codeUrl }));
// 使用App的提示
window.Toaster.postMessage(JSON.stringify({ message: '提示資訊' }));
複製程式碼

JavascriptChannelname就是window方法需要通訊的方法名,onMessageReceived方法的引數就是postMessage傳送的引數。

網頁不能開啟三方應用問題

網頁中會有開啟第三方應用、撥打電話的功能,在我們不對WebView增加方法的時候點選就會無效。話不多說現在開始解決這個問題。

WebView中增加navigationDelegate這個方法可以監聽到導航地址的變化。

navigationDelegate: (NavigationRequest request) {
  debugPrint('request.url ${request.url}');
  // 檢查支付寶
  if (request.url.contains('alipays://')) {
    _openUrl(request.url);
    return NavigationDecision.prevent;
  }
  // 路由攔截-單頁應用路由檢測有問題
  if (request.url.contains('https://3gimg.qq.com/') ||
      request.url.contains('https://apis.map.qq.com')) {
    debugPrint('跳轉地圖-路由被攔截');
    return NavigationDecision.prevent;
  }
  if (request.url.contains('tel:')) {
    // 撥打電話
    _openUrl(request.url);
    return NavigationDecision.prevent;
  }
  debugPrint('allowing navigation to ${request.url}');
  return NavigationDecision.navigate;
}
複製程式碼

_openUrl方法

使用url_launcher外掛的launch方法可以開啟App或者瀏覽器,開啟App使用的是schema

Future<void> _openUrl(String linkUrl) async {
  await launch(linkUrl);
}
複製程式碼

結語

目前就是我使用Webview遇到的問題,大家遇到有其它問題歡迎留言討論。

相關文章