Flutter 78: 圖解 Android Native 集成 FlutterBoost 小嘗試 (一)

??????小菜前幾天剛將歷史項目升級至 AndroidX 并接入 Flitter Module,接下來小菜準備采用 flutter_boost 進行 Native 與 Flutter 兩端交互;小菜從未接觸過 FlutterBoost,為了研究方便,小菜特意新建兩個工程單獨學習基本的映射和跳轉;

Module 集成

1. 新建 AndroidX 工程

??????小菜新建一個 AndroidX 工程,其中 minSdkVersion >= 16,等待接入 Flutter Module;

compileSdkVersion 28
defaultConfig {
    applicationId "com.ace.flutter.ace_demo02"
    minSdkVersion 16
    targetSdkVersion 28
    versionCode 1
    versionName "1.0"
    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

2. 新建 Flutter Module

??????小菜新建一個 Flutter Module 集成到 Android Project 中;其中該 Module 也支持 AndroidX;

compileSdkVersion 28
defaultConfig {
    applicationId "com.ace.flutter.flutter_module04.host"
    minSdkVersion 16
    targetSdkVersion 28
    versionCode 1
    versionName "1.0"
    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

3. Flutter Module 中接入 FlutterBoost

??????小菜按照官網嘗試接入 'v0.1.61-androidx-hotfixes' 分支 FlutterBoost 發現并未完全適配 AndroidX,于是切換至較新的 'v1.12.13-hotfixes' 分支,Packages get 檢驗,可以正常運行;

flutter_boost:
  git:
    url: 'https://github.com/alibaba/flutter_boost.git'
    ref: 'v1.12.13-hotfixes'

4. AndroidX Project 接入 Flutter Module

??????小菜將 Flutter Module 接入到 Android 工程中,方法不再贅述,注意 build.gradle 中需要加入 flutterflutter_boost 兩個依賴;Sync 之后 Project 中會加入 Flutter 和 FlutterBoost 模塊;

implementation project(':flutter')
implementation project(':flutter_boost')
    
setBinding(new Binding([gradle: this]))
evaluate(new File(
        '/Users/user/Documents/ACE_FLUTTER/flutter_module04/.android/include_flutter.groovy'
))

Code 案例

??????至此,Flutter 和 FlutterBoost 的集成已基本完成,接下來是兩端映射與跳轉方面的學習,小菜建議剛開始時可以將官網的代碼復制拷貝到項目中,先跑通項目更直觀的感受;小菜為了學習逐步滲透;

Android 端

??????根據 FlutterBoost 官網用法,首先需要在 Application 中初始化 FlutterBoost;無論是 Flutter 之間路由跳轉還是 FlutterNative 之間路由跳轉均需要通過 INativeRouter 接口進行交互;

private void initFlutterBoost() {
    INativeRouter router = new INativeRouter() {
        @Override
        public void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode, Map<String, Object> exts) {
            String assembleUrl = Utils.assembleUrl(url, urlParams);
            PageRouter.openPageByUrl(context, assembleUrl, urlParams);
        }
    };

    FlutterBoost.BoostLifecycleListener boostLifecycleListener = new FlutterBoost.BoostLifecycleListener() {
        @Override
        public void onEngineCreated() { }

        @Override
        public void onPluginsRegistered() { }

        @Override
        public void onEngineDestroy() { }
    };
    Platform platform = new FlutterBoost
            .ConfigBuilder(this, router)
            .isDebug(true)
            .whenEngineStart(FlutterBoost.ConfigBuilder.ANY_ACTIVITY_CREATED)
            .renderMode(FlutterView.RenderMode.texture)
            .lifecycleListener(boostLifecycleListener)
            .build();
    FlutterBoost.instance().init(platform);
}

??????其中路由管理是由公共的 PageRouter 文件管理;提供了通用的 openPageByUrl,根據用戶提供的 url 與設置好的映射集合進行對比,確認一致之后通過 startActivity() 進行頁面跳轉;若需要傳遞 Bundle 參數的話,可以通過 Map 類型進行傳遞;

public static boolean openPageByUrl(Context context, String url, Map params, int requestCode) {
    String path = url.split("\\?")[0];
    try {
        if (pageName.containsKey(path)) {
            Intent intent = BoostFlutterActivity.withNewEngine().url(pageName.get(path)).params(params).backgroundMode(BoostFlutterActivity.BackgroundMode.opaque).build(context);
            if(context instanceof Activity){
                Activity activity=(Activity)context;
                activity.startActivityForResult(intent,requestCode);
            }else{
                context.startActivity(intent);
            }
            return true;
        }
        return false;
    } catch (Throwable t) {
        return false;
    }
}

??????公共路由中需要 BoostFlutterActivity 作為容器進行處理,因此需要在 AndroidManifest.xml 中注冊;其中 SplashScreenDrawable 作為路由跳轉時背景效果,可以按照需要進行調整;

<activity
    android:name="com.idlefish.flutterboost.containers.BoostFlutterActivity"
    android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
    android:hardwareAccelerated="true"
    android:theme="@style/Theme.AppCompat"
    android:windowSoftInputMode="adjustResize">
    <meta-data
        android:name="io.flutter.embedding.android.SplashScreenDrawable"
        android:resource="@drawable/ic_launcher" />
</activity>

Flutter 端

??????無論是 Android 還是 Flutter 均需要初始化,在 main.dartbuild 方法中初始化;小菜新建了兩個測試 Page,其中路由映射的 url 要與 Android Native 端一致;同時還提供了 NavigatorObserver 進行前后路由的監聽;

@override
void initState() {
  super.initState();

  FlutterBoost.singleton.registerPageBuilders({
    'first_page': (pageName, params, _) => FirstPage(),
    'second_page': (pageName, params, _) => SecondPage()
  });
  FlutterBoost.singleton.addBoostNavigatorObserver(TestBoostNavigatorObserver());
}

@override
Widget build(BuildContext context) {
return MaterialApp(
        title: 'Flutter Boost example',
        builder: FlutterBoost.init(postPush: _onRoutePushed),
        home: Container( color: Colors.white,
            child: Center(child: Text('${_result}', style: TextStyle(color: Colors.blueAccent, fontSize: 18.0)))));
}

void _onRoutePushed(String pageName, String uniqueId, Map params, Route route, Future _) {}

Route 跳轉

??????路由跳轉主要是 NativeFlutter 兩端之間雙向的交互,小菜分為如下方式進行測試;

Android -> Android 跳轉

??????通過 openPageByUrl 中分析 Native 之間的跳轉依舊是通過系統的 startActivity 來進行處理,小菜不做過多贅述;

startActivity(new Intent(A.this, B.class));
Android -> Flutter 跳轉

??????AndroidFlutter 通過 BoostFlutterActivity 構建跳轉,注意映射 url 一致;若需要獲取返回值內容,可以通過 **** 固定的 KEY 獲取,且獲取的格式是 Object 格式;

Intent intent = BoostFlutterActivity.withNewEngine().url(pageName.get(path)).params(params).backgroundMode(BoostFlutterActivity.BackgroundMode.opaque).build(context);
if (context instanceof Activity) {
    Activity activity = (Activity) context;
    activity.startActivityForResult(intent,requestCode);
} else {
    context.startActivity(intent);
}

// Native 跳轉 FirstPage (無參)
PageRouter.openPageByUrl(this, "first_page", null);
===============================================================================
// Native 跳轉 FirstPage (有參)
Map map = new HashMap();
map.put("params_name", "張三");
map.put("params_age", 18);
PageRouter.openPageByUrl(this, "first_page", map);
===============================================================================
// Native 跳轉 SecondPage (有參 + 返回值)
PageRouter.openPageByUrl(this, "second_page", map, 101);

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (data != null && data.getExtras() != null) {
        Toast.makeText(this, data.getExtras().get(IFlutterViewContainer.RESULT_KEY).toString(), Toast.LENGTH_LONG).show();
    }
}
Flutter -> Flutter 跳轉

??????Flutter 之間的跳轉可以通過默認的 Navigator 方式,也可以通過 FlutterBoost.singleton.open 方式進行頁面跳轉;注意跳轉的頁面均需在 main.dart 中提前映射好;

// FirstPage 跳轉 SecondPage (無參)
FlutterBoost.singleton.open('second_page');

===============================================================================
// FirstPage 跳轉 SecondPage (有參)
FlutterBoost.singleton.open('second_page', urlParams: {'params_name': '李四', 'params_age': 28});
===============================================================================
// FirstPage 跳轉 SecondPage (有參 + 返回值)
FlutterBoost.singleton.open('second_page', urlParams: { 'params_name': '李四', 'params_age': 28  })
    .then((Map value) { print('Second Page 頁面銷毀時獲取的返回結果 result =  $value'); });
Flutter -> Android 跳轉

??????FlutterNative 的跳轉需要根據不同映射的 url 單獨判斷;其中接收參數通過 openPageByUrlparams 獲取;若由 FlutterNative 需要返回值,注意頁面跳轉時使用 startActivityForResult 方式,且關閉 Native 時傳參的 KEY 為固定的 IFlutterViewContainer.RESULT_KEY;

if (path.startsWith("native://")) {
    if (path.equals("native://main_activity")) {
        Intent intent = new Intent(context, MainActivity.class);
        intent.putExtra("params_from_flutter", params.toString());
        // context.startActivity(intent);  // 無返回值
        Activity activity = (Activity) context;
        activity.startActivityForResult(intent, requestCode);  // 有返回值
    }
    return true;
}

// SecondPage 跳轉 MainActivity (無參)
FlutterBoost.singleton.open('native://main_activity');
===============================================================================
// SecondPage 跳轉 MainActivity (有參)
FlutterBoost.singleton.open('native://main_activity', urlParams: {'params_name': '王五', 'params_age': 38});
===============================================================================
// SecondPage 跳轉 MainActivity (有參 + 返回值)
FlutterBoost.singleton.open('native://main_activity', urlParams: { 'params_name': '王五', 'params_age': 38 })
    .then((Map value) {  Toast.show('MainActivity 頁面銷毀時獲取的返回結果 result =  $value', context, duration: Toast.LENGTH_SHORT, gravity:  Toast.BOTTOM); });
// MainActivity
Map map = new HashMap();
map.put("params_name", "趙六");
map.put("params_age", 48);
Intent intent = new Intent();
intent.putExtra(IFlutterViewContainer.RESULT_KEY, (Serializable) map);
setResult(Activity.RESULT_OK, intent);
finish();

核心源碼

class FirstPage extends StatelessWidget {
  final Map params;
  FirstPage({this.params});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('First Page')),
        body: Column(mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max,
            children: <Widget>[
              Center(child: Text('params 為:${params ?? "null"}')),
              Center(child: RaisedButton(child: Text('打開 Second Flutter Page (無參)'),
                      onPressed: () { FlutterBoost.singleton.open('second_page'); })),
              Center(child: RaisedButton(child: Text('打開 Second Flutter Page (有參)'),
                      onPressed: () { FlutterBoost.singleton.open('second_page',
                            urlParams: {'params_name': '李四', 'params_age': 28}); })),
              Center(child: RaisedButton(
                      child: Text('打開 Second Flutter Page (有參 + 返回值)'),
                      onPressed: () {
                        FlutterBoost.singleton.open('second_page', urlParams: {
                          'params_name': '李四', 'params_age': 28
                        }).then((Map value) {
                          print('Second Page 頁面銷毀時獲取的返回結果 result =  $value');
                        });
                      }))
            ]));
  }
}

class SecondPage extends StatelessWidget {
  final Map params;
  SecondPage({this.params});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('Second Page')),
        body: Column(mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max,
            children: <Widget>[
              Center(child: Text('params 為:${params ?? 'null'}')),
              Center(child: RaisedButton(child: Text('關閉 Second Flutter Page 且帶返回值'),
                      onPressed: () { 
                        BoostContainerSettings settings = BoostContainer.of(context).settings;
                        FlutterBoost.singleton.close(settings.uniqueId, result: {'result': '來自 SecondPage Result'});
                      })),
              Center(child: RaisedButton(child: Text('打開 Native MainActivity (無參)'),
                      onPressed: () { FlutterBoost.singleton.open('native://main_activity'); })),
              Center(child: RaisedButton(child: Text('打開 Native MainActivity (有參)'),
                      onPressed: () { FlutterBoost.singleton.open('native://main_activity',
                            urlParams: {'params_name': '王五', 'params_age': 38}); })),
              Center(child: RaisedButton(
                      child: Text('打開 Native MainActivity (有參 + 返回值)'),
                      onPressed: () {
                        FlutterBoost.singleton.open('native://main_activity',
                            urlParams: { 'params_name': '王五', 'params_age': 38
                            }).then((Map value) {
                          Toast.show('MainActivity 頁面銷毀時獲取的返回結果 result =  $value', context, duration: Toast.LENGTH_SHORT, gravity:  Toast.BOTTOM);
                        });
                      }))
            ]));
  }
}

??????小菜對 FlutterBoost 的了解還知之甚少,且每個版本的方法注意點各有不同,如有錯誤請多多指導!

來源: 阿策小和尚

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容