flutter 解决webview加载重定向h5页面 返回重复加载问题
long time no see. 如果觉得该方案helps,点个赞,评论打个call,这是我前进的动力~
通常写法:
项目里用的webview_flutter
正常webview处理返回事件
if (await controller.canGoBack()) {
controller.goBack();
} else {
Navigator.pop(context);
}
就是h5历史栈,一直退栈,如果栈内元素只有一个了,就直接关闭webview的页面了。
问题描述:
正常情况是没问题的的。
比如A-->B-->C,一直触发返回事件的话,逻辑是C-->B,B-->A, A直接关。
如果h5里有重定向的话,就有问题了。
比如A(A1重定向到A2)-->B-->C,一直触发返回事件的话,逻辑是C-->B,B-->A2, A2-->A1-->A2,A2-->A1-->A2...
导致webview界面一直退不出来。
解决方案:
参考https://github.com/flutter/flutter/issues/137737,拉到最下面
设定pageFinished后xxx毫秒内NavigationRequest触发,判定为重定向。逻辑:已知A1重定向A2,此时触发返回事件,A2返回到A1,在A1准备重定向到A2的时候,根据条件判断为重定向然后进行阻断,并再次执行一次返回逻辑。
另外该issue原始代码还是有问题,没有考虑到NavigationRequest可能跑在onPageFinished前面,故自己添加了轮询等待的代码。
注意:这只是workaround,极端情况下并不能做到100%可靠。必要情况可以考虑跟h5相关开发,约定不用重定向或改用其它方案。
自己在android设备上实测了下,还是挺稳定的。
几种可以考虑的方案:
1.修改flutter_webview源码,上传到github,然后在自己的仓库引用该库。(该方案可以自己去修改到android测和ios测的相关代码,比如flutter_webview没提供忽略ssl证书报错和ssl证书检查的问题就可以通过该方式解决,感兴趣的话可以上网查一查)
2.换webview的库,比如用flutter_inappwebview,该库提供更强大的原生api支持,围绕这个库的api来尝试解决。也是很流行的库,但不是官方flutter.dev出品。
解决代码:
如下
class WebPageContainer extends StatefulWidget {
const WebPageContainer({super.key});
@override
State<WebPageContainer> createState() => _WebPageContainerState();
}
class _WebPageContainerState extends State<WebPageContainer> {
late WebViewController controller;
String url = '';
bool _backEventTriggered = false;
DateTime? _lastedPageFinishedTime;
bool _pageIsFinished = false;
@override
void initState() {
super.initState();
}
@override
void didChangeDependencies() {
final Map<String, dynamic>? arguments = ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
if (arguments != null) {
url = arguments['url'] ?? '';
debugPrint('third---url:$url');
}
super.didChangeDependencies();
_initWebViewController();
}
// web端调用
// <button onclick="jump()">打开一个新的webpage</button>
// function jump() {
// var msg = "https://www.baidu.com"
// if (toNewWebPage) {
// toNewWebPage.postMessage(msg);
// }
// }
// getStatusBarHeight用法
// h5页面调用getStatusBarHeight,同上
// h5页面同时要定义onStatusBarHeightReceived,该方法是flutter测获取完高度后调用的
// 例如:
// function onStatusBarHeightReceived(height) {
// // 显示状态栏高度
// document.getElementById('statusBarHeight').innerText = 'Status Bar Height: ' + height;
// }
void _initWebViewController() {
controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00000000))
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
// debugPrint('WebPage onProgress $progress');
},
onPageStarted: (String url) {
_pageIsFinished = false;
debugPrint('WebPage onPageStarted $url');
},
onPageFinished: (String url) async {
debugPrint('WebPage onPageFinished $url');
_pageIsFinished = true;
if (_backEventTriggered) {
_lastedPageFinishedTime = DateTime.now();
} else {
_lastedPageFinishedTime = null;
}
},
onWebResourceError: (WebResourceError error) {},
onNavigationRequest: (NavigationRequest request) async {
debugPrint('WebPage onNavigationRequest ${request.url}');
debugPrint('WebPage onNavigationRequest isMainFrame ${request.isMainFrame}');
//轮询,因为onNavigationRequest可能跑在onPageFinished前面,强制等待
while (!_pageIsFinished) {
await Future.delayed(Duration(milliseconds: 10));
}
if (_shouldApplyNavLockout()) {
goBack(); //执行第二次back
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
onUrlChange: (UrlChange change) {
print('WebPage onUrlChange ${change.url}');
}),
)
..addJavaScriptChannel('destoryCurrentPage', onMessageReceived: (JavaScriptMessage message) {
//h5自己的返回键,返回到最后一步,当前页面出栈
debugPrint('====destoryCurrentPage====');
Nav.pop();
})
..addJavaScriptChannel('toNewWebPage', onMessageReceived: (JavaScriptMessage message) {
//允许h5页面打开新的third_web_page
Nav.push(routerName: RouterPathModuleCommon.WebPageContainer, arguments: {'url': message.message});
})
..addJavaScriptChannel('toLogin', onMessageReceived: (JavaScriptMessage message) {
//login:有些h5页面跳转后需要登录的 logout:可能存在的h5页面提供登出功能
Nav.push(routerName: RouterPathModuleAccount.LoginPage, arguments: {'url': message.message});
})
..addJavaScriptChannel('getStatusBarHeight', onMessageReceived: (JavaScriptMessage message) {
double statusBarHeight = MediaQuery.of(context).padding.top;
controller.runJavaScriptReturningResult("onStatusBarHeightReceived('$statusBarHeight')").then((value) => print("发送statusBarHeight成功"));
});
controller.loadRequest(Uri.parse(url));
}
// 判断重定向的条件: 最近一次pageFinished和navigationRequest小于xxx毫秒。 这只是个workaround,并不是十全十美的方案
bool _shouldApplyNavLockout() {
final timestamp = _lastedPageFinishedTime;
_lastedPageFinishedTime = null;
// TODO make the threshold time configurable.
if (timestamp != null) {
debugPrint('WebPage diff timestamp ${DateTime.now().difference(timestamp!)}');
}
return timestamp != null && DateTime.now().difference(timestamp) < const Duration(milliseconds: 150);
}
void goBack() async {
if (await controller.canGoBack()) {
_backEventTriggered = true;
controller.goBack();
} else {
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: WillPopScope(
onWillPop: () async {
goBack();
return false;
},
child: WebViewWidget(controller: controller),
),
);
}
}