Flutter集成到Android項(xiàng)目三部曲

本文主要解決3個(gè)問題:

  1. 集成Flutter到Android項(xiàng)目,可以打開Flutter的默認(rèn)頁(yè)面
  2. 可以跳轉(zhuǎn)到Flutter的指定頁(yè)面
  3. 可以將Flutter的指定組件嵌入到原生頁(yè)面,并傳遞參數(shù)

1.集成Flutter到Android

這里,我們以Flutter Module創(chuàng)建一個(gè)Flutter工程(flutter),然后run起來(lái),就可以在.android/Flutter/build/outouts/aar文件夾下面得到這個(gè)aar


這里之所以以Flutter Module模式開發(fā),而不是Flutter Application,就是為了得到這個(gè)aar。
Flutter Module模式下自動(dòng)生成的.android文件夾下,才會(huì)有這個(gè)Flutter文件夾,F(xiàn)lutter Application則沒有。
這樣的話,我們才可以借用Flutter已經(jīng)有的生成aar的gradle腳本,不然還得自己去寫gradle打包腳本,很容易踩到坑里就爬不起來(lái)了。

然后我們?cè)倭黹_一個(gè)窗口,新建一個(gè)Android工程(flutter_container),將這個(gè)aar復(fù)制過(guò)去


這里需要注意的一個(gè)問題,因?yàn)镕lutter本身原因,導(dǎo)致復(fù)制出來(lái)的aar里面缺少icudtl.dat文件,需要我們自己手動(dòng)復(fù)制這個(gè)icudtl.dat文件到assets/flutter_shared目錄下。

怎么得到這個(gè)icudtl.dat文件呢,很簡(jiǎn)單,解壓Flutter工程生成的默認(rèn)apk即可得到


然后,我們就需要在宿主Android工程里面,建立接收Flutter的Activity了。這里可以借鑒Flutter工程的.android/app目錄,核心就是兩個(gè):

  1. Application:初始化Flutter
public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        FlutterMain.startInitialization(this);
    }
}
  1. Activity:繼承FlutterActivity
/**
 * debug模式原生跳轉(zhuǎn)到flutter界面會(huì)出現(xiàn)白屏,release包就不會(huì)出現(xiàn)白屏了
 */
public class MainFlutterActivity extends FlutterActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
  }
}

這樣以后,我們就可以跳轉(zhuǎn)這個(gè)MainFlutterActivity,實(shí)現(xiàn)在Android工程里面進(jìn)入Flutter工程的默認(rèn)頁(yè)面了。

2. 跳轉(zhuǎn)指定頁(yè)面

上面只是簡(jiǎn)單集成了Flutter,但是我們知道,我們從Android工程里面跳轉(zhuǎn)Flutter,肯定是需要選擇性的跳轉(zhuǎn)指定頁(yè)面的,不可能只是簡(jiǎn)單的跳轉(zhuǎn)默認(rèn)頁(yè)面就完了,所以,這里需要用到Flutter的靜態(tài)路由了。

修改Flutter工程的main.dart,定義了兩個(gè)指定頁(yè)面的路由:homePage、channelPage

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: TestPage(),
      //這種方式不能傳遞參數(shù),主要是方便原生調(diào)用
      routes: <String, WidgetBuilder> {
        'homePage': (BuildContext context) => new HomePage(),
        'channelPage': (BuildContext context) => new ChannelPage(),
      },
    );

  }
}

然后在宿主Android工程下,添加指定頁(yè)面的容器Activity,通過(guò)Flutter.createView來(lái)獲取指定頁(yè)面的View

注意,這里的HomeFlutterActivity只需要繼承AppCompatActivity 即可,不需要繼承FlutterActivity了。

public class HomeFlutterActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FlutterView homePage = Flutter.createView(
                this,
                getLifecycle(),
                "homePage"
        );
        setContentView(homePage);
    }
}

這樣以后,我們就可以跳轉(zhuǎn)這個(gè)HomeFlutterActivity,實(shí)現(xiàn)在Android工程里面進(jìn)入Flutter工程的指定頁(yè)面了。

3. 嵌入View并傳遞參數(shù)

上面雖然能夠跳轉(zhuǎn)指定頁(yè)面了,但是很顯然,有一個(gè)很大的問題:不能傳遞參數(shù)。

這是Flutter的靜態(tài)路由的一個(gè)很大的弊端,雖然通過(guò)動(dòng)態(tài)路由可以傳遞參數(shù)和接收返回值,但是動(dòng)態(tài)路由沒法給原生調(diào)用。

 Navigator.of(context)
                .push<String>(new MaterialPageRoute(builder: (context) {
              return new NextPage(params);
            })).then((String value) {
              setState(() {
                params = value;
              });
            });

有一個(gè)Flutter的路由庫(kù):Fluro,可以實(shí)現(xiàn)靜態(tài)路由傳參,例如這樣:

傳參

var bodyJson = '{"user":1281,"pass":3041}';
router.navigateTo(context, '/home/$bodyJson');

接收

Router router = new Router();

void main() {
  router.define('/home/:data', handler: new Handler(
      handlerFunc: (BuildContext context, Map<String, dynamic> params) {
        return new FluroHomePage(params['data'][0]);
      }));
  runApp(MyApp());
}

但是,這種方式在Flutter內(nèi)部還行,卻無(wú)法給原生調(diào)用,在原生里面通過(guò)Flutter.createView的時(shí)候,是沒法使用Fluro的,只能是默認(rèn)的路由。

調(diào)研了很多方案,最后,沒有辦法了,只好采用最笨的方法:通過(guò)MethodChannel來(lái)傳遞參數(shù)。

這里需要注意的是MethodChannel的調(diào)用,應(yīng)該FlutterView已經(jīng)創(chuàng)建完成,所以需要通過(guò)flutterView.post(new Runnable())來(lái)執(zhí)行了,直接執(zhí)行是不會(huì)傳參給Flutter的。

原生傳參給Flutter

原生調(diào)用

MethodChannel channel = new MethodChannel(flutterView, CHANNEL);
channel.invokeMethod("invokeFlutterMethod", "hello,flutter", new MethodChannel.Result() {
    @Override
    public void success(@Nullable Object o) {
        Log.i("flutter","1.原生調(diào)用invokeFlutterMethod-success:"+o.toString());
    }
    @Override
    public void error(String s, @Nullable String s1, @Nullable Object o) {
        Log.i("flutter","1.原生調(diào)用invokeFlutterMethod-error");
    }
    @Override
    public void notImplemented() {
        Log.i("flutter","1.原生調(diào)用invokeFlutterMethod-notImplemented");
    }
});

Flutter執(zhí)行

platform.setMethodCallHandler((handler) {
      Future<String> future=Future((){
        switch (handler.method) {
          case "invokeFlutterMethod":
            String args = handler.arguments;
            print("2.Flutter執(zhí)行invokeFlutterMethod:${args}");
            return "this is flutter result";
        }
      });
      return future;
    });

Flutter傳參給原生

Flutter調(diào)用

print("3.Flutter調(diào)用invokeNativeMethod");
int result =
    await platform.invokeMethod("invokeNativeMethod", "hello,native");
print("5.收到原生執(zhí)行結(jié)果:${result}");

原生執(zhí)行

channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
    @Override
    public void onMethodCall(MethodCall call, MethodChannel.Result result) {
        switch (call.method) {
            case "invokeNativeMethod":
                String args = (String) call.arguments;
                Log.i("flutter","4.原生執(zhí)行invokeNativeMethod:"+args);
                result.success(200);
                break;
            default:
        }
    }
});

最后貼一下這個(gè)傳參頁(yè)面的完整代碼吧,主要就是跑了一下:

  1. 原生調(diào)用invokeFlutterMethod
  2. Flutter執(zhí)行invokeFlutterMethod
  3. Flutter調(diào)用invokeNativeMethod
  4. 原生執(zhí)行invokeNativeMethod

Android:

public class ChannelFlutterActivity extends AppCompatActivity {

    private static final String CHANNEL = "com.ezbuy.flutter";

    FlutterView flutterView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_channel);
        FrameLayout frFlutter = findViewById(R.id.fr_flutter);
        flutterView = getFlutterView("channelPage");
        frFlutter.addView(flutterView);
        flutterView.post(new Runnable() {
            @Override
            public void run() {
                initMethodChannel(flutterView);
            }
        });
    }

    public FlutterView initMethodChannel(FlutterView flutterView) {
        MethodChannel channel = new MethodChannel(flutterView, CHANNEL);
        //1.原生調(diào)用Flutter方法
        channel.invokeMethod("invokeFlutterMethod", "hello,flutter", new MethodChannel.Result() {
            @Override
            public void success(@Nullable Object o) {
                Log.i("flutter","1.原生調(diào)用invokeFlutterMethod-success:"+o.toString());
            }

            @Override
            public void error(String s, @Nullable String s1, @Nullable Object o) {
                Log.i("flutter","1.原生調(diào)用invokeFlutterMethod-error");
            }

            @Override
            public void notImplemented() {
                Log.i("flutter","1.原生調(diào)用invokeFlutterMethod-notImplemented");
            }
        });
        Log.i("flutter","1.原生調(diào)用invokeFlutterMethod");
        //4.Flutter調(diào)用原生方法的監(jiān)聽
        channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(MethodCall call, MethodChannel.Result result) {
                switch (call.method) {
                    case "invokeNativeMethod":
                        String args = (String) call.arguments;
                        Log.i("flutter","4.原生執(zhí)行invokeNativeMethod:"+args);
                        result.success(200);
                        break;
                    default:
                }
            }
        });
        return flutterView;
    }

    public FlutterView getFlutterView(String initialRoute) {
        return Flutter.createView(
                this,
                getLifecycle(),
                initialRoute
        );
    }
}

Flutter

class ChannelPage extends StatefulWidget {

  ChannelPage();

  @override
  _ChannelPageState createState() => _ChannelPageState();
}

class _ChannelPageState extends State<ChannelPage> {
  static const platform = const MethodChannel('com.ezbuy.flutter');

  String data;

  @override
  void initState() {
    super.initState();
    data ="默認(rèn)data";
    initChannel();
  }

  @override
  Widget build(BuildContext context) {
    //必須用Scaffold包裹
    return Scaffold(body: new Center(child: new Text(data)));
  }

  void initChannel() {
    platform.setMethodCallHandler((handler) {
      Future<String> future=Future((){
        switch (handler.method) {
          case "invokeFlutterMethod":
            String args = handler.arguments;
            print("2.Flutter執(zhí)行invokeFlutterMethod:${args}");
            setState(() {
              data = "2.Flutter執(zhí)行invokeFlutterMethod:${args}";
            });
            invokeNativeMethod();
            return "this is flutter result";
        }
      });
      return future;
    });
  }

  void invokeNativeMethod() async {
    print("3.Flutter調(diào)用invokeNativeMethod");
    int result =
        await platform.invokeMethod("invokeNativeMethod", "hello,native");
    print("5.收到原生執(zhí)行結(jié)果:${result}");
  }

}

對(duì)啦,我們這節(jié)說(shuō)的是將Flutter以View級(jí)別嵌套在一個(gè)Android的Activity里面,其實(shí)很簡(jiǎn)單了啊,因?yàn)槲覀兺ㄟ^(guò)Flutter.createView創(chuàng)建出來(lái)的View和普通的View并沒有什么太大的區(qū)別,直接addView就可以了,沒啥特別操作,比如這個(gè)ChannelFlutterActivity,我用的布局文件就是如下所示:

最后的執(zhí)行效果就是:


其它坑

1. Flutter工程依賴了插件時(shí),宿主Android工程會(huì)報(bào)找不到插件的原生代碼的錯(cuò)誤

我的Flutter工程依賴了shared_preferences插件,導(dǎo)致報(bào)錯(cuò):

原因是:Flutter工程導(dǎo)出成aar的時(shí)候,沒有包含插件里面的原生代碼。

解決方案有2種,網(wǎng)上說(shuō)是不用默認(rèn)的生成aar的方式,用fataar-gradle-plugin來(lái)讓生成的flutter.aar直接包含嵌套的插件工程的aar,這就需要修改Flutter工程的.android/Flutter/build.gradle文件了。我試過(guò),結(jié)果報(bào)了循環(huán)依賴的錯(cuò)誤,就放棄了,大家如果這個(gè)方案走通了,歡迎告知我具體步驟。

我的解決方案:這里我采取了一個(gè)簡(jiǎn)單粗暴直接的方案,直接找到插件的aar,將它也復(fù)制到宿主Android工程了。這個(gè)插件的aar在這里:


復(fù)制到這里:


但是這個(gè)方案的弊端就是,以后每一個(gè)插件,你都需要復(fù)制一下,后期的維護(hù)成本是有點(diǎn)高的。不像fataar是一勞永逸,只有flutter.aar這一份aar的。尤其是后期肯定會(huì)將aar做成遠(yuǎn)程依賴,而不再是直接發(fā)復(fù)制過(guò)去,那維護(hù)成本就更高了些。

結(jié)語(yǔ)

通過(guò)上文可以看到,其實(shí)Flutter集成到Android項(xiàng)目還是挺方便的(除了FlutterView傳參有點(diǎn)麻煩)。至于Flutter如何集成到ios項(xiàng)目,我還沒有實(shí)踐過(guò),還需要和ios的同事探索,如果你在集成到ios項(xiàng)目的過(guò)程中,填了哪些坑,有哪些經(jīng)驗(yàn)總結(jié),歡迎和我們交流。

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

推薦閱讀更多精彩內(nèi)容