Appearance
10路由设计:Flutter中是如何实现Scheme跳转的
上一课时我们已经创建好了项目的基础框架结构,其中有一个 router.dart 文件,该文件的作用是实现 App 内的一个路由管理和跳转。本课时是基于该功能模块,着重介绍如何实现 App 内外的路由跳转。
Scheme
在介绍路由跳转实现之前,我们先来了解下 Scheme 的概念,Scheme 是一种 App 内跳转协议,通过 Scheme 协议在 APP 内实现一些页面的互相跳转。一般可以使用以下格式协议。
java
[scheme]://[host]/[path]?[query]
这种格式不仅可以使用在 App 内部实现可跳转,还可以适用于外部拉起 App 指定页面的功能。内部跳转类似于前端的页面路由,只不过前端的页面路由是直接用链接来处理,在 App 中是无法像前端一样能够使用链接实现路由管理。外部跳转则需要分别从 Android 和 iOS 来介绍,其中主要的不同点是一些平台配置,下面我们先来看看内部如何实现路由跳转。
内部跳转
按照上面的协议规则,我们将 Scheme 设置为项目 App 名字 tyfApp ,由于是 App 跳转 host 可以不设置,path 为具体的 pages 页面,query 则为 pages 需要的参数,举例如下。
java
tyfapp://userPageIndex?userId=1001
基础版本
根据这个规则,实现 router.dart 逻辑。首先需要 import 相应的 pages 页面,例如这里我们需要两个页面的跳转和一个 Web 页面。
dart
import 'package:two_you_friend/pages/common/web_view_page.dart';
import 'package:two_you_friend/pages/home_page/index.dart';
import 'package:two_you_friend/pages/user_page/index.dart';
web_view_page.dart 使用第三方库,在遇到 http 或者 https 的协议时,使用该页面去打开,具体代码如下:
dart
import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
/// 通用跳转逻辑
class CommonWebViewPage extends StatelessWidget {
/// url 地址
final String url;
/// 构造函数
CommonWebViewPage({Key key, this.url}) : super(key: key);
@override
Widget build(BuildContext context) {
return WebviewScaffold(
url: url,
appBar: AppBar(
backgroundColor: Colors.blue,
),
);
}
}
使用第三方库 flutter_webview_plugin 来打开具体的网页地址,该组件一个是 url 具体的网址,一个是 appBar 包含 title 和主题信息。如果非网页地址,并符合页面规范的协议时,我们需要解析出跳转的页面以及页面需要的参数。
dart
/// 解析跳转的url,并且分析其内部参数
Map<String, dynamic> _parseUrl(String url) {
if(url.startsWith(appScheme)) {
url = url.substring(9);
}
int placeIndex = url.indexOf('?');
if(url == '' || url == null) {
return {'action': '/', 'params': null};
}
if (placeIndex < 0) {
return {'action': url, 'params': null};
}
String action = url.substring(0, placeIndex);
String paramStr = url.substring(placeIndex + 1);
if (paramStr == null) {
return {'action': action, 'params': null};
}
Map params = {};
List<String> paramsStrArr = paramStr.split('&');
for (String singleParamsStr in paramsStrArr) {
List<String> singleParamsArr = singleParamsStr.split('=');
params[singleParamsArr[0]] = singleParamsArr[1];
}
return {'action': action, 'params': params};
}
上面这段代码和前端解析 url 的代码逻辑完全一致,使用 ? 来分割 path 和参数两部分,再使用 & 来获取参数的 key 和 value。解析出 path 和页面参数后,我们需要根据具体的 path 来跳转到相应的组件页面,并将解析出来的页面参数带入到组件中,代码如下。
dart
/// 根据url处理获得需要跳转的action页面以及需要携带的参数
Widget _getPage(String url, Map<String, dynamic> urlParseRet) {
if (url.startsWith('https://') || url.startsWith('http://')) {
return CommonWebViewPage(url: url);
} else if(url.startsWith(appScheme)) {
// 判断是否解析出 path action,并且能否在路由配置中找到
String pathAction = urlParseRet['action'].toString();
switch (pathAction) {
case "homepage": {
return _buildPage(HomePageIndex());
}
case "userpage": {
// 必要性检查,如果没有参数则不做任何处理
if(urlParseRet['params']['user_id'].toString() == null) {
return null;
}
return _buildPage(
UserPageIndex(
userId: urlParseRet['params']['user_id'].toString()
)
);
}
default: {
return _buildPage(HomePageIndex());
}
}
}
return null;
}
首先就是判断是否以 http 和 https 开头,如果是则使用网页打开,如果不是则继续判断是否符合 App Scheme 信息,符合则解析出相应的 path 和参数信息,并且根据 path 调用对应的页面组件,打开相应的页面信息。如果匹配不到或者不符合 App Scheme 则不做任何处理。
最后为该 Router 类添加一个可外部调用的函数,执行页面跳转,代码如下。
dart
/// 执行页面跳转
void push(BuildContext context, String url) {
Map<String, dynamic> urlParseRet = _parseUrl(url);
// 不同页面,则跳转
Navigator.push(context, MaterialPageRoute(builder: (context) {
return _getPage(url, urlParseRet);
}));
}
上面逻辑会存在一些问题,主要问题是没有考虑到当前页面,无论什么情况下都会打开一个新的页面,这样会很耗费机器资源,接下来我就介绍下如何优化这块逻辑。
进阶版本
进阶版本的目的是判断当前是否有打开页面,如果打开了页面则替换和刷新旧页面,如果没有则打开新的页面。分为以下两点来分析:
了解页面标识,具体打开的页面路由名称;
判断当前打开的页面,如果已经打开则更新,未打开则新建窗口;
首先,我们需要使用到 Flutter 路由注册功能,我们需要修改 main.dart 中的代码,在 build MaterialApp 组件中增加两个函数,代码如下:
dart
return MaterialApp(
title: 'Two You', // APP 名字
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue, // APP 主题
),
routes: Router().registerRouter(),
home: Scaffold(
appBar: AppBar(
title: Text('Two You'), // 页面名字
),
body: Center(
child: Entrance(),
)));
上面代码中的第 7 行是注册路由名字,我们看下 Router 中的实现。
dart
/// 注册路由事件
Map<String, Widget Function(BuildContext)> registerRouter () {
return {
'homepage' : (context) => _buildPage(HomePageIndex()),
'userpage' : (context) => _buildPage(UserPageIndex())
};
}
按照这个方式注册其他的页面信息即可,接下来我们着重看下 push 的方法。
dart
/// 执行页面跳转
void push(BuildContext context, String url) {
Map<String, dynamic> urlParseRet = _parseUrl(url);
Navigator.pushNamedAndRemoveUntil(
context,
urlParseRet['action'].toString(), (route) {
if(route.settings.name ==
urlParseRet['action'].toString()) {
return false;
}
return true;
}, arguments: urlParseRet['params']);
}
}
在原来的基础上我们修改了方法,使用到了 pushNamedAndRemoveUntil 方法,并且在第二个回调参数中判断是否为当前页面,如果是则返回 false,否则返回 true。这种方式有个缺点就是在具体的 pages 页面中不能直接通过构造函数去获取参数,必须使用下面的方式。
dart
@override
Widget build(BuildContext context) {
Map dataInfo = JsonConfig.objectToMap(
ModalRoute.of(context).settings.arguments
);
// TODO: implement build
return Text('I am use page ${dataInfo['userId']}');
}
获取的方式是第 3 到 5 行,这段逻辑还必须在 build 中,存在一定缺陷,现阶段还没有找到其他的解决方案,后续有解决方案再通过源码进行更新。
以上就是整个 router.dart 的实现逻辑,这样就可以在 APP 内的页面实现跳转,接下来我们看看如何在 App 外也能使用这个 Scheme,拉起 App。
外部跳转
该功能的实现,需要使用 uni_links 第三方库来协助完成外部页面的 Scheme,在 pubspec.yaml 中增加依赖,然后更新本地库文件。由于 Android 和 iOS 在配置上会有点区别,因此这里分别来介绍。
Android 流程
在项目中找到这个路径下的文件
java
android/app/src/main/AndroidManifest.xml
在配置的 application 下的 activity 内增加如下配置:
xml
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:scheme="tyfapp"/>
</intent-filter>
第 6 行代码就是声明这个 App 的 Scheme 的协议。
iOS 流程
在项目中找到这个路径下的文件
java
ios/Runner/info.plist
在 dict 内增加下面的配置:
xml
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>Two You</string>
<key>CFBundleURLSchemes</key>
<array>
<string>tyfapp</string>
</array>
</dict>
</array>
其中的第 9 行声明 App 的 Scheme。
以上就完成了基础的配置,接下来我们就使用 uni_links 来实现 Scheme 的监听。
Uni_links 实现外部跳转
首先我们在 pages 目录下新建一个主入口文件 entrance.dart ,该文件需要设计为一个有状态类组件。在组件中最关键的是监听获取到打开 App 的链接地址,实现的方式如下代码。
dart
/// 使用[String]链接实现
Future<void> initPlatformStateForStringUniLinks() async {
String initialLink;
// Platform messages may fail, so we use a try/catch PlatformException.
try {
initialLink = await getInitialLink();
if (initialLink != null) {
// 跳转到指定页面
router.push(context, initialLink);
}
} on PlatformException {
initialLink = 'Failed to get initial link.';
} on FormatException {
initialLink = 'Failed to parse the initial link as Uri.';
}
// Attach a listener to the links stream
_sub = getLinksStream().listen((String link) {
if (!mounted || link == null) return;
// 跳转到指定页面
router.push(context, link);
}, onError: (Object err) {
if (!mounted) return;
});
其中第 6 行是处理在外部直接拉起 App 的业务逻辑,第 17 行则表示当前 App 处于打开状态,监听外部拉起事件,监听变化后处理相应的跳转逻辑。由于组件中有一个监听事件,为了避免组件被销毁后还在监听,因此需要在组件销毁阶段移除监听事件,代码如下:
dart
@override
void dispose() {
super.dispose();
if (_sub != null) _sub.cancel();
}
为了验证效果,使用了一个 github 上创建的测试页面。接下来我们运行下程序,然后在手机模拟器中打开测试页面,可以看到如图 1 所示的效果。
图 1 Scheme 实现运行效果
以上就实现了 Scheme 可以直接在内外部使用的跳转逻辑。不过 Scheme 在 App 外部存在一些体验方面的问题,比如:
当需要被拉起的 App 没有被安装时,这个链接就不会生效;
在大部分 App 内 Scheme 是被禁用的,因此在用户体验的时候会非常差;
注册的 Scheme 相同导致冲突;
为了解决上述问题,Andorid 和 iOS 都提供了一套解决方案,在 Android 叫作 App link / Deep links ,在 iOS 叫作 Universal Links / Custom URL schemes。解决的方案就是在未安装 App 时可提供网页跳转,其次可以使用 https 和 http 域名链接的方式来进一步提升唯一性。
App link / Deep links
应用链接仅适用于 https 方案,并且需要指定的主机以及托管文件 assetlinks.json,该配置文件可参考如下:
json
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example",
"sha256_cert_fingerprints":
["14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"]
}
}]
package_name,在应用的 build.gradle 文件中声明的应用 ID;
sha256_cert_fingerprints:应用签名证书的 SHA256 指纹,你可以利用 Java 密钥工具。
配置好该文件后,同样是修改下面路径下的文件。
java
android/app/src/main/AndroidManifest.xml
在配置的 application 下的 activity 内增加如下配置:
xml
<!-- App Links -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with https://YOUR_HOST -->
<data
android:scheme="https"
android:host="[YOUR_HOST]" />
</intent-filter>
具体的过程,你可以在线上项目开发过程中尝试应用。
Universal Links / Custom URL schemes
该方法也是需要一个主机域名来启动应用,因此需要服务的一个在线配置,例如:https://www.example.test/apple-app-site-association 获取 apple-app-site-association 的配置文件如下。
java
{
"applinks": {
"apps": [],
"details": [
{
"appID": "8LX3M43WHV.me.gexiao.me",
"paths": [ "/*" ]
}
]
}
}
同样我们需要修改下面路径的文件。
java
ios/Runner/info.plist
在 dict 内增加下面的配置:
xml
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:[YOUR_HOST]</string>
</array>
以上就是外部跳转的实现方案,实现外部跳转的 App Links 和 Universal Link 功能,由于需要域名部署,我这里就没有实际应用,具体你可以在项目开发中尝试。
总结
本课时介绍了在 Flutter 中路由跳转以及外部 Scheme 启动 App 的方法,最后简单介绍了 App Links 和 Universal Link 的知识点。学完本课时你需要掌握开发 Flutter 路由跳转基础技巧,并且能够应用 uni_links 库实现外部 Scheme 启动 App 功能。
下一课时我将介绍 Flutter 中各种导航栏的设计,我会在本课时的基础上增加导航栏功能,其次我也会实现首页和个人页面的代码逻辑。