今天,来聊聊通过短信、邮件中的链接打开跳转到APP,并打开特定的Flutter页面。在做原生开发的时候,我们经常可能碰到这种情况,一般是为了拉新,或者网页业务跳转等。通过点击某个超链接跳转到APP,并打开相应的页面。
前言
这其实也不难,在Android端和iOS实现也差不多,配置schema、host和path等等。Android端有deep Link和App Link两种方式,至于有啥区别,怎么做?这篇就不说了,网上都处都有,可以去查一下。iOS端不是很熟悉,了解到也是两种,一种是配置scheme;另一种是通过open url。应该和Android端大同小异,大家都可以去查一查。(Android端可以查看:https://developer.android.com/training/app-links/deep-linking)
这里主要是聊一下,接受到Open url或者说schema打开APP的数据,怎么去跳转到Flutter写的特定页面,也就是原生端收到跳转数据,如何在Flutter端打开特定的页面。
一、原生与Flutter端通信
我们应该可想到,像通过schema或者open url打开Android或是iOS应用,这无非是Android和iOS原生系统实现的功能,那么也就是说,通过schema或者open Url打开App,也只能是原生端收到了消息。
那么问题来了,如果是根据某个参数或者path不同,需要打开Flutter端中的某个页面,那么Flutter端怎么知道呢?那么这就涉及到了原生与Flutter端的通信问题了,关于原生与Flutter的通信有三种方式:在这篇文章里有介绍《Flutter与原生如何进行通信》。
这里我们需要用到Event Channel,来从原生端发送消息给Flutter端,Flutter端监听到Open Url消息后,跳转到相应页面。
二、Android原生端处理
2.1 先定义好Event Channel Name,并注册链接Event Channel
class MainActivity: FlutterActivity() {
//定义好Event Channel url
private val EVENT_CHANNEL = "***.**/event/channel"
private var appLinkEventSink: EventChannel.EventSink? = null
private var appLinkData: Map<String, Any?>? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
...
//注册链接Event Channel
registerAppLinkEventChannel(flutterEngine.dartExecutor)
}
private fun registerAppLinkEventChannel(dartExecutor: DartExecutor) {
val appLinkEventChannel = EventChannel(dartExecutor, EVENT_CHANNEL)
appLinkEventChannel.setStreamHandler(object : EventChannel.StreamHandler{
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
//与Flutter建立链接后,将EventChannel.EventSink赋值给全局变量
appLinkEventSink = events
sendLinkDataToFlutter()
}
override fun onCancel(arguments: Any?) {
appLinkEventSink = null
}
})
}
private fun sendLinkDataToFlutter() {
if(appLinkEventSink != null && appLinkData != null){
//给Flutter端发送数据
appLinkEventSink?.success(appLinkData)
appLinkData = null
}
}
}
2.2 在MainActivity中接受到Open Url数据后,组装需要发送给Flutter端的数据
我这里是用了一个Util类来专门组装,发送给Flutter端的数据类型可以是用一个字典(Map)组装。
class AppLinkUtil {
companion object{
fun getAppLinkData(appLinkUri: Uri?, context: Context): Map<String, Any?>? {
var dataMap: Map<String, Any?>? = null;
var appLinkType = getAppLinkType(appLinkUri, context)
if(appLinkType != -1) {
dataMap = HashMap()
dataMap["appLinkType"] = getAppLinkType(appLinkUri, context)
dataMap["data"] = getAppLinkParamData(appLinkUri)
dataMap["url"] = appLinkUri?.path
}
return dataMap
}
private fun getAppLinkParamData(appLinkUri: Uri?): Map<String, Any?>? {
val path = appLinkUri?.path
var paramData: Map<String, Any?>? = null
if(path != null){
paramData = HashMap()
val parameterNames: Set<String>? = appLinkUri.queryParameterNames
if (parameterNames != null) {
for(parameterName in parameterNames){
paramData[parameterName] = appLinkUri.getQueryParameter(parameterName)
}
}
}
return paramData
}
private fun getAppLinkType(appLinkUri: Uri?, context: Context): Int {
var appLinkeType = -1
val path = appLinkUri?.path
if(path != null){
if(path.equals("path A")){
appLinkeType = AppLinkType.A.ordinal
}else if(path.equals("path B")){
appLinkeType = AppLinkType.B.ordinal
}
}
return appLinkeType
}
}
}
enum class AppLinkType{
A,
B
}
2.3 将组装数据发送给Flutter端
class MainActivity: FlutterActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
handleIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
/**
* Deal With Third part intent to app
*/
private fun handleIntent(intent: Intent) {
val appLinkAction = intent.action
val appLinkUri: Uri? = intent.data
if(Intent.ACTION_VIEW == appLinkAction && !launchedActivityFromHistory(intent)){
appLinkData = AppLinkUtil.getAppLinkData(appLinkUri, this)
//Tips 1:
// 因为在收到Open Url数据的时候,与Flutter端的Event Channel链接可能还没建立好,所以需要将要发送的数据用全局变量保存
// 判断是否已经链接好了,在发送
sendLinkDataToFlutter()
}
}
//Tips 2:
// 这里一定要判断App是否是从History里启动的,因为从History(Remote)启动的话,会将上一次的intent重复发送,所以会出现重复打开页面的情况
private fun launchedActivityFromHistory(intent: Intent?): Boolean {
return intent != null && intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
}
private fun sendLinkDataToFlutter() {
if(appLinkEventSink != null && appLinkData != null){
//给Flutter端发送数据
appLinkEventSink?.success(appLinkData)
appLinkData = null
}
}
}
这里主要有两个需要注意的点:
Tips 1: 因为在收到Open Url数据的时候,与Flutter端的Event Channel链接可能还没建立好,所以需要将要发送的数据用全局变量保存。
if(Intent.ACTION_VIEW == appLinkAction && !launchedActivityFromHistory(intent)){
appLinkData = AppLinkUtil.getAppLinkData(appLinkUri, this)
//Tips 1:
// 因为在收到Open Url数据的时候,与Flutter端的Event Channel链接可能还没建立好,所以需要将要发送的数据用全局变量保存
// 判断是否已经链接好了,在发送
sendLinkDataToFlutter()
}
Tips 2: 这里一定要判断App是否是从History里启动的,因为从History(Remote)启动的话,会将上一次的intent重复发送,所以会出现重复打开页面的情况。
//Tips 2:这里一定要判断App是否是从History里启动的,因为从History(Remote)启动的话,会将上一次的intent重复发送,所以会出现重复打开页面的情况
// 这里一定要判断App是否是从History里启动的,因为从History(Remote)启动的话,会将上一次的intent重复发送,所以会出现重复打开页面的情况
private fun launchedActivityFromHistory(intent: Intent?): Boolean {
return intent != null && intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
}
三、iOS原生端处理
3.1 先定义好Event Channel Name,并注册链接Event Channel
@objc class AppDelegate: FlutterAppDelegate,FlutterStreamHandler {
var flutterEventSink : FlutterEventSink?;
var dict:NSMutableDictionary = NSMutableDictionary()
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
...
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
//开始链接Event channel
startEventChannel(binaryMessenger: controller.binaryMessenger);
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func onListen(withArguments arguments: Any?,
eventSink: @escaping FlutterEventSink) -> FlutterError? {
//与Flutter建立链接后,将EventChannel.EventSink赋值给全局变量
flutterEventSink = eventSink;
openUrlSink();
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
flutterEventSink = nil;
return nil
}
func startEventChannel(binaryMessenger: FlutterBinaryMessenger) {
let eventChannel = FlutterEventChannel(name: "**.**/eventchannel",binaryMessenger: binaryMessenger)
eventChannel.setStreamHandler(self)
}
}
3.2 在AppDelegate中接受到Open Url数据后,组装需要发送给Flutter端的数据
enum SkipType :Int {
case A = 0
case B = 1
}
override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
dict.setValue(url.absoluteString, forKey: "url");
if url.host == K_**_HOST {
if url.path.hasSuffix(PathA) {
dict.setValue(SkipType.A.rawValue,forKey: K_SKIPTYPE_KEY);
openUrlSink();
}else if url.path.hasSuffix(PathB) {
dict.setValue(SkipType.B.rawValue,forKey: K_SKIPTYPE_KEY);
let data:NSMutableDictionary = NSMutableDictionary()
let myArray : Array? = url.query?.components(separatedBy: "&")
for i in myArray ?? []{
let myArray2 = i.components(separatedBy:"=")
data.setValue(myArray2[1], forKey: myArray2[0])
}
if data.count > 0 {
dict.setValue(data,forKey: "data");
openUrlSink();
}
}
}
return true;
}
3.3 组装跳转数据并发送给Flutter端
func openUrlSink() {
if self.flutterEventSink != nil && self.dict.count > 0{
eventSink(data: dict as! Dictionary<String, Any>);
self.dict.removeAllObjects();
}
}
func eventSink(data: Dictionary<String, Any>) {
self.flutterEventSink?(data);
}
其实在iOS端也是一样的,因为有可能在我们接收到Open Url的时候,Event Channel并没有链接好,所以任然需要将需要传输到Flutter端的数据保存为全局数据,并判断是否已经链接好,链接好了才发送数据。
四、Flutter端接收跳转数据,并执行具体逻辑跳转到相应页面
4.1 监听接收原生端传过来的Open Url参数数据
在我的需求中,我在main.dart中监听了Event Channel,并将数据缓存到了SharedPreference中,根据不同需求,也是可以直接取数据跳转的
class AppChannel {
static const eventChannelName = '**.**/event/channel';
static const EventChannel eventChannel =
const EventChannel(AppChannel.eventChannelName);
static startListenOpenUrl() {
try {
eventChannel
.receiveBroadcastStream()
.listen((value) {
if(value != null){
SharedPreferences.getInstance().then(
(sharedPreferences) {
sharedPreferences.setString(ConstantString.prefs_app_link_data, jsonEncode(value)).then((value) {
Future.delayed(Duration(milliseconds: 1000), (){
AppLinkUtil.dealWithAppLinkData();
});
});
}
);
}
}, onError: (error){
});
} on Exception catch (e) {
LogUtil.d(e);
}
}
}
4.2 根据参数跳转到具体页面
这里话就是具体需求,具体逻辑了,比如说有可能没登录呀,需要登录后跳转;有的又可以直接跳转等,看具体需求,实现逻辑了。这里就是简单示例跳转页面:
static void navigatorToTestPage(AppLinkData appLinkData) {
String dataA = appLinkData.dataA;
bool dataB = appLinkData?.dataB ?? true;
Navigator.pushNamed(globalKeyNavigatorKey.currentState.context,
'TestPage',
arguments: {
'DataA': dataA,
'DataB': dataB
});
}
五、结语
通过Schema或者Open Url跳转到Flutter具体页面也就收工了,写文章越来越顺手了~ O(∩_∩)O哈哈~
申明:禁用于商业用途,如若转载,请附带原文链接。https://www.jianshu.com/p/a9d49fe637ba蟹蟹~
PS: 写文不易,觉得没有浪费你时间,请给个关注和点赞~ 😁