如何通过短信、链接等跳转到APP,并打开相应Flutter页面

  今天,来聊聊通过短信、邮件中的链接打开跳转到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? = 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? {
            var dataMap: Map? = 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? {
            val path = appLinkUri?.path
            var paramData: Map? = null
            if(path != null){
                paramData = HashMap()
                val parameterNames: Set? = 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);
       self.dict.removeAllObjects();
    }
}

func eventSink(data: Dictionary) {
   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: 写文不易,觉得没有浪费你时间,请给个关注和点赞~

你可能感兴趣的:(如何通过短信、链接等跳转到APP,并打开相应Flutter页面)