從零開始的Flutter之旅: MethodChannel

午後一小憩發表於2020-07-09

從零開始的Flutter之旅: MethodChannel

往期回顧

從零開始的Flutter之旅: StatelessWidget

從零開始的Flutter之旅: StatefulWidget

從零開始的Flutter之旅: InheritedWidget

從零開始的Flutter之旅: Provider

從零開始的Flutter之旅: Navigator

flutter_github有這麼一個場景:通過authorization認證方式進行登入。而authorization的具體登入形式是,通過跳轉一個網頁連結進行github授權登入,成功之後會攜帶對應的code到指定客戶端中,然後客戶端可以通過這個code來進行oauth授權登入,成功之後客戶端可以拿到該賬戶的token,所以之後的github操作都可以通過該token來進行請求。由於token是有時效性,同時也可以手動解除授權,所以相對於在客戶端進行賬戶密碼登入來說更加安全。

從零開始的Flutter之旅: MethodChannel

那麼要實現上面這個場景,Flutter就需要與原生客戶端進行通訊,拿到返回的code,然後再到Flutter中進行oauth授權登入請求。

通訊方式可以使用MethodChannel,這個就是今天的主題。

OAuth App

authorization認證的原理已經知道了,下面直接來看實現方案。

首先我們需要一個OAuth App用來提供使用者通過github授權的應用。

這個在github上可以直接註冊的

從零開始的Flutter之旅: MethodChannel

在註冊的OAuth App時會有一個Authorization callback URL必填項。這個callback url的作用就是當你通過該連結認證通過後會以App Link的方式使用該url跳轉到對應的App應用,同時返回認證成功的code。這裡將其定義為REDIRECT_URI

註冊成功之後,我們拿到它的Client IDClient SecretAuthorization callback URL,拼接成下面的連線

const String URL_AUTHORIZATION =
    'https://github.com/login/oauth/authorize?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=user%20repo%20notifications%20';
複製程式碼

有了跳轉到外部的認證連結之後,下面就是在應用中實現這個跳轉認證流程。

url_launcher

首先需要跳轉外部瀏覽器訪問上面的authorization連結。這一步的實現需要藉助url_launcher,它能夠幫助我們檢查連結是否有效,同時啟動外部瀏覽器進行跳轉。

在使用之前需要在pubspec.yaml中新增依賴

dependencies:
  flutter:
    sdk: flutter
  http: 0.12.0+4
  dio: 3.0.7
  shared_preferences: 0.5.6+1
  url_launcher: 5.4.1
  ...
複製程式碼

依賴成功之後,使用canLaunch()來檢查連結的有效性;launch()來啟動跳轉

  authorization() {
    return () async {
      FocusScope.of(context).requestFocus(FocusNode());
      if (await canLaunch(URL_AUTHORIZATION)) {
        // 為設定forceSafariVC,IOS 預設會開啟APP內部WebView
        // 而APP內部WebView不支援重定向跳轉到APP
        await launch(URL_AUTHORIZATION, forceSafariVC: false);
      } else {
        throw 'Can not launch $URL_AUTHORIZATION)';
      }
    };
  }
複製程式碼

Scheme

通過authorization()方法可以成功跳轉到外部瀏覽器進行登入授認證。授權成功之後會返回到之前的app,具體頁面路徑與連結中配置的REDIRECT_URI有關。

const String REDIRECT_URI = 'github://login';
複製程式碼

這裡定義了一個Scheme,為了能夠成功返回到客戶端指定的頁面,我們需要為Android與IOS配置對應的Scheme。

Android

找到AndroidManifest檔案,在activity便籤下新增intent-filter屬性

<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:host="login"
        android:scheme="github" />
</intent-filter>
複製程式碼

前面的actioncategory配置是固定的,如果需要支援不同的scheme,主要修改的是data中的配置。

schemehost分別對應到REDIRECT_URI中的數值。

IOS

找到info.plist檔案,新增URL types便籤,在它的item下配置對應的URL identifierURL Schemes

從零開始的Flutter之旅: MethodChannel

配置完scheme之後,就能夠正常返回到對應的客戶端頁面。

接下來需要考慮的是,如何拿到返回的code值

MethodChannel

這個時候今天的主角就該上場了。

MethodChannel簡單的說就是Flutter提供與客戶端通訊的渠道,使用時互相約定一個渠道name與對應的呼叫客戶端指定方法的method

所以我們先來約定好這兩個值

const String METHOD_CHANNEL_NAME = 'app.channel.shared.data';
const String CALL_LOGIN_CODE = 'getLoginCode';
複製程式碼

然後通過MethodChannel來獲取對應的渠道

  callLoginCode(AppLifecycleState state) async {
    if (state == AppLifecycleState.resumed) {
      final platform = const MethodChannel(METHOD_CHANNEL_NAME);
      final code = await platform.invokeMethod(CALL_LOGIN_CODE);
      if (code != null) {
        _getAccessTokenFromCode(code);
      }
    }
  }
複製程式碼

使用invokeMethod來呼叫客戶端對應的方法,這裡是用來獲取授權成功後返回客戶端的code。

這是Flutter呼叫客戶端方法的步驟,下面再看客戶端的實現

Android

首先我們將約定好的渠道名稱與回撥方法名定義為常量

object Constants {
    const val AUTHORIZATION_CODE = "code"
    const val METHOD_CHANNEL_NAME = "app.channel.shared.data"
    const val CALL_LOGIN_CODE = "getLoginCode"
}
複製程式碼

在之前我們已經在AndroidManifest.xml中定義的scheme,所以認證成功後回返回客戶端的MainActivity頁面,同時回撥onNewIntent方法。

所以獲取返回code的方式可以在onNewIntent中進行,同時還需要建立對應的MethodChannel與提供回撥的方法。具體實現如下:

class MainActivity : FlutterActivity() {
 
    private var mAuthorizationCode: String? = null
 
    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)
        setupMethodChannel()
    }
 
    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        getExtra(intent)
    }
 
    private fun getExtra(intent: Intent?) {
        // from author login
        mAuthorizationCode = intent?.data?.getQueryParameter(Constants.AUTHORIZATION_CODE)
    }
 
    private fun setupMethodChannel() {
        MethodChannel(flutterEngine?.dartExecutor, Constants.METHOD_CHANNEL_NAME).setMethodCallHandler { call, result ->
            if (call.method == Constants.CALL_LOGIN_CODE && !TextUtils.isEmpty(mAuthorizationCode)) {
                result.success(mAuthorizationCode)
                mAuthorizationCode = null
            }
        }
    }
}
複製程式碼

MethodChannel建立渠道,setMethodCallHandler來響應Flutter中需要呼叫的方法。通過判斷回撥的方法名稱,即之前在Flutter中約定的CALL_LOGIN_CODE。來執行對應的邏輯

因為我們需要返回的code值,只需通過resultsuccess方法,將獲取到的code傳遞過去即可。之後Flutter就能夠獲取到該值。

IOS

AppDelegate.swift中定義一個methodChannel,使用約定好的name。

methodChannel的建立IOS是通過FlutterMethodChannel.init來生成。之後的回撥與Android的基本類似

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    var paramsMap: Dictionary<String, String> = [:]
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
     
    let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
    let methodChannel = FlutterMethodChannel.init(name: "app.channel.shared.data", binaryMessenger: controller.binaryMessenger)
     
    methodChannel.setMethodCallHandler { (call, result) in
        if "getLoginCode" == call.method && !self.paramsMap.isEmpty {
            result(self.paramsMap["code"])
            self.paramsMap.removeAll()
        }
    }
     
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
    override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        let absoluteString = url.absoluteURL.absoluteString
        let urlComponents = NSURLComponents(string: absoluteString)
        let queryItems = urlComponents?.queryItems
        for item in queryItems! {
            paramsMap[item.name] = item.value
        }
        return true
    }
}
複製程式碼

setMethodCallHandler中判斷回撥的方法是否與約定的方法名一致,如果一致再通過result方法將code傳遞給Flutter。

至此Android與IOS都與Flutter建立了通訊,它們之間的橋樑就是通過MethodChannel來搭建的。

最後code傳回到Flutter之後,我們再將code進行請求獲取到對應的token。

到這裡整個授權認證就完成了,之後我們就可以通過token來請求使用者相關的介面,獲取對應的資料。

token的獲取與相關介面的呼叫可以通過檢視flutter_github原始碼獲取

flutter_github

flutter_github,這是一個基於Github Open Api開發的Flutter版本的Github客戶端。該專案主要是用來練習Flutter,感興趣的可以加入一起來學習,如果有幫助的話也請不要吝嗇你的關注。

從零開始的Flutter之旅: MethodChannel

當然如果你想了解Android原生,AwesomeGithub是一個不錯的選擇。它是flutter_github的純Android版本。

如果你喜歡我的文章模式,或者對我接下來的文章感興趣,你可以關注我的微信公眾號:【Android補給站】

或者掃描下方二維碼,與我建立有效的溝通,同時能夠更方便的收到相關的技術推送。

從零開始的Flutter之旅: MethodChannel

相關文章