忙しい方向け
Q. iosでonPageFinishedが呼ばれない
A. URLが無効な場合は呼ばれない。用途によるがonUrlChangeやonWebResourceErrorを使うと代用できるかも。OSごとに動きが違う可能性を頭に入れておくべし。
onUrlChange: (UrlChange change) {
debugPrint('url change to ${change.url}');
},
onWebResourceError: (WebResourceError error) {
debugPrint('Page resource error: ${error.url}');
},
はじめに
アドベントカレンダー22日目!playground所属、学部4年生のko_chaです。参加したハッカソンの中でお世話になった(苦しめられた)webview_flutterについて書きます。
https://pub.dev/packages/webview_flutter
Tokyo Flutter Hackathon
11/2,3開催@Abema Towers
https://tokyo-flutter-hackathon.connpass.com/event/326665/
チームメンバーのアドベントカレンダー
バックエンド担当のTatsuroさん
https://blog.shinonome.io/dong-jing/
同じくモバイル担当のyoさん
25日担当です!
他の学生の記事もぜひご覧ください。
https://qiita.com/advent-calendar/2024/playground
それでは早速本題に入ります!
ios版のみアクセストークン取得ができない
やりたかったこと:onPageFinishedが呼ばれた時にクエリからトークンを取得
しかしなぜかios版だけonPageFinishedが呼ばれないという現象に苦しめられました。
調べている内に「URLが無効だとonPageFinishedが呼ばれない」というissueを見つけたので、これが原因だと仮定して再現してみました。(かなり古いissue)
https://github.com/flutter/flutter/issues/74987
再現
使用したコードは最後に載せています。
ログイン時のリダイレクトは短縮URLを作るサービスbitlyを用いて再現しました。
アクセスしたら"https://flutter.devv/" という存在しないページに飛ばされるようにしています。
PCブラウザで開くとこうなります。ちゃんと(?)無効
余談ですがhttps://www.flutter.de/は存在するようです笑
実際の動き
Android
I/flutter (16437): Page started loading: https://flutter.devv/
I/flutter (16437): Page finished loading: https://flutter.devv/
ios
flutter: Page started loading: https://bit.ly/49Wjv5i
android版だとonPageStartedの時点でリダイレクト後のURLが表示される⭐️android版ではエラーに関係なくonPageFinishedが呼ばれる。
⭐️ios版だとエラーが起きた際にはonPageFinishedが表示されない。
対策
対策としては以下の2つを提案します。
- onUrlChangeを使い、エラー処理を待たずURLを取得する方法
- onWebResourceErrorを使い、(Android版と同じように)エラーが起きていたとしてもfinish扱いで処理する
onUrlChange: (UrlChange change) {
debugPrint('url change to ${change.url}');
},
onWebResourceError: (WebResourceError error) {
debugPrint('Page resource error: ${error.url}');
},
どちらのOSでも検知できるので、今回のようにとりあえずクエリを取りたいだけであれば有用かと存じます。
対策後の動き
Android
I/flutter (16437): Page started loading: https://flutter.devv/
I/flutter (16437): Page resource error: https://flutter.devv/ ←追加分
I/flutter (16437): Page finished loading: https://flutter.devv/
I/flutter (16437): url change to https://flutter.devv/ ←追加分
ios
flutter: url change to https://bit.ly/49Wjv5i ←追加分
flutter: Page started loading: https://bit.ly/49Wjv5i
flutter: url change to https://flutter.devv/ ←追加分
flutter: Page resource error: https://flutter.devv/ ←追加分
flutter: url change to null ←追加分
ちょっと深掘り
ご興味があればお付き合いください。
そもそもwebview_flutterの中身って?
A Flutter plugin that provides a WebView widget.On iOS the WebView widget is backed by a WKWebView. On Android the WebView widget is backed by a WebView.
とあるように、実は裏側では全く別のものが動いているようです。
それぞれの呼び出されるタイミングを少し調べてみましょう。
webview_flutter_androidの裏側
webview_flutterとAndroidネイティブのWebViewがどう繋がっているか見ていきます。
①アンドロイド版のwebviewを司るパッケージ
このパッケージが、②PlatformViewのTLHCというモードを使ってAndroidネイティブUIを表示しています。
PlatformViewについてはこちらの記事が分かりやすかったです。
③Dart側にurl等の情報を送信: AndroidWebkitLibrary.g.kt
fun onPageStarted(
pigeon_instanceArg: android.webkit.WebViewClient,
webViewArg: android.webkit.WebView,
urlArg: String,
callback: (Result<Unit>) -> Unit
) {
if (pigeonRegistrar.ignoreCallsToDart) {
callback(
Result.failure(
AndroidWebKitError("ignore-calls-error", "Calls to Dart are being ignored.", "")))
return
}
val binaryMessenger = pigeonRegistrar.binaryMessenger
val codec = pigeonRegistrar.codec
val channelName = "dev.flutter.pigeon.webview_flutter_android.WebViewClient.onPageStarted"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(pigeon_instanceArg, webViewArg, urlArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(
Result.failure(
AndroidWebKitError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
callback(Result.success(Unit))
}
} else {
callback(Result.failure(createConnectionError(channelName)))
}
}
}
④呼び出し元: WebViewClient
WebViewClientクラスを継承したWebViewClientImplクラスがネイティブのWebViewから状態の変更を受け取っています。
WebViewClientImplクラスのonPageStarted関数をオーバーライドし、dart側に送信する動きに書き換えてあります。
public class WebViewClientProxyApi extends PigeonApiWebViewClient {
/** Implementation of {@link WebViewClient} that passes arguments of callback methods to Dart. */
@RequiresApi(Build.VERSION_CODES.N)
public static class WebViewClientImpl extends WebViewClient {
private final WebViewClientProxyApi api;
private boolean returnValueForShouldOverrideUrlLoading = false;
/**
* Creates a {@link WebViewClient} that passes arguments of callbacks methods to Dart.
*
* @param api handles sending messages to Dart.
*/
public WebViewClientImpl(@NonNull WebViewClientProxyApi api) {
this.api = api;
}
@Override
public void onPageStarted(@NonNull WebView view, @NonNull String url, @NonNull Bitmap favicon) {
api.getPigeonRegistrar()
.runOnMainThread(() -> api.onPageStarted(this, view, url, reply -> null));
}
@Override
public void onPageFinished(@NonNull WebView view, @NonNull String url) {
api.getPigeonRegistrar()
.runOnMainThread(() -> api.onPageFinished(this, view, url, reply -> null));
}
~~~
中略
~~~
}
}
このようにAndroid版でonPageStarted,onPageFinishedが呼ばれるタイミングはネイティブのWebViewClientクラスに由来するので、タイミングもネイティブと同じだと考えられます。
ios版も少しだけ
先ほど検証したFlutterでの動きと同じように、ページ読み込みに失敗した場合は終了扱いにならないようです。(didFinishが呼ばれない)
https://stackoverflow.com/questions/71619574/wkwebview-does-not-call-didfinish-delegate-method
ios版の内部実装が気になる方は、こちらからご確認ください。
https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_wkwebview
結論
簡単にまとめるとこんな感じでしょうか。
Android | ios | |
---|---|---|
onPageStarted | URL: リダイレクト後 | URL: リダイレクト前 |
onPageFinished | エラー発生時も実行 URL: リダイレクト後 |
エラー発生時は実行されない |
onUrlChange | URL:リダイレクト後のみ | URL: リダイレクト前、後、エラー時はnullが入る |
まとめ
「クロスプラットフォーム」が当たり前ではないことを実感できた良い機会でした。
ただ割と見つけやすい差異だったので、未だにこちら側のミスを疑っています。
他にもissueを立てていた人がいたので違うと信じたいですが、、
3年前のissueが直されていないということは、こういう仕様で決定なのかも...?
何はともあれパッケージを作ってくださった作者様に感謝。
いつかOSSにPRを出せたら最高ですね。
明日は当団体の大エース@TAK848さんです!
サンプルコード
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
const requestUrl = 'https://bit.ly/49Wjv5i';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const WebViewExample(),
);
}
}
class WebViewExample extends StatefulWidget {
const WebViewExample({super.key});
@override
State<WebViewExample> createState() => _WebViewExampleState();
}
class _WebViewExampleState extends State<WebViewExample> {
late final WebViewController controller;
@override
void initState() {
controller =
WebViewController()
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (String url) {
print('Page started loading: $url');
},
onPageFinished: (String url) {
print('Page finished loading: $url');
},
onUrlChange: (UrlChange change) {
debugPrint('url change to ${change.url}');
},
onWebResourceError: (WebResourceError error) {
debugPrint('Page resource error: ${error.url}');
},
),
)
..loadRequest(Uri.parse(requestUrl));
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(body: WebViewWidget(controller: controller));
}
}
動作環境
webview_flutter: ^4.8.0
flutterバージョン
$ flutter --version
Flutter 3.28.0-0.1.pre • channel beta • https://github.com/flutter/flutter.git
Framework • revision 3e493a3e4d (9 days ago) • 2024-12-12 05:59:24 +0900
Engine • revision 2ba456fd7f
Tools • Dart 3.7.0 (build 3.7.0-209.1.beta) • DevTools 2.41.0