Flutter學(xué)習(xí)小計(jì):Android原生項(xiàng)目引入Flutter

前言
目前Flutter可以說(shuō)是非?;馃崃?,多次更新過(guò)后也越來(lái)越穩(wěn)定,受到了很多開(kāi)發(fā)者的青睞。不過(guò)純Flutter開(kāi)發(fā)還是存在一定成本和風(fēng)險(xiǎn)的,尤其是對(duì)于規(guī)模稍大一些的項(xiàng)目,可能更加適合的是將Flutter用于項(xiàng)目中的某一個(gè)模塊,因此我們有必要了解一下如何在原生項(xiàng)目中引入Flutter。

本文介紹一下Android原生項(xiàng)目引入Flutter的方法以及Flutter如何與原生進(jìn)行交互,包括頁(yè)面間的跳轉(zhuǎn)和方法的調(diào)用,本人不懂IOS開(kāi)發(fā),有需要的話還是自行百度吧o(╥﹏╥)o,但是基本思路我覺(jué)得不會(huì)差太多的。

Android原生項(xiàng)目中引入Flutter

這應(yīng)該是目前Flutter在實(shí)際開(kāi)發(fā)中應(yīng)用最多的一種場(chǎng)景,在已有的Android原生項(xiàng)目中引入Flutter,針對(duì)一些復(fù)雜的頁(yè)面,使用Flutter開(kāi)發(fā)可以有效地提高開(kāi)發(fā)效率。
官方提供的文檔Add Flutter to existing apps詳細(xì)介紹了原生app引入Flutter的步驟,不過(guò)很遺憾是英文的。我也是參考了網(wǎng)上的一些相關(guān)文章,總結(jié)了一下文檔中的提到的幾個(gè)步驟。

  • 第一步、新建Android項(xiàng)目

這沒(méi)什么可說(shuō)的,畢竟我們是要在原生項(xiàng)目中引入Flutter嘛。

  • 第二步、新建Flutter Module

有兩種方式來(lái)創(chuàng)建Flutter Module,第一種是通過(guò)命令行來(lái)創(chuàng)建,首先切換到Android項(xiàng)目的同級(jí)目錄下,執(zhí)行以下命令:

flutter create -t module my_flutter

其中my_flutter為module的名字。第二種是直接使用Android Studio來(lái)創(chuàng)建,依次點(diǎn)擊左上角的File --> New --> New Flutter Project,然后選擇Flutter Module。

新建Flutter Module.png

然后填寫module的名稱、路徑。

最后填寫module的包名,點(diǎn)擊Finish就創(chuàng)建好了一個(gè)Flutter Module。

  • 第三步、在Android項(xiàng)目中引入Flutter Module

首先在app下的build.gradle文件中添加以下配置:

compileOptions {
  sourceCompatibility 1.8
  targetCompatibility 1.8
}

我們知道這是使用Java 8所需要的配置,在這里的作用是為了解決版本兼容問(wèn)題,如果不配置的話運(yùn)行項(xiàng)目可能會(huì)報(bào)錯(cuò):Invoke-customs are only supported starting with Android O (--min-api 26)
然后在項(xiàng)目根目錄下的setting.gradle文件中配置:

include ':app'
// 加入下面配置
setBinding(new Binding([gradle: this]))
evaluate(new File(
        settingsDir.parentFile,
        'my_flutter/.android/include_flutter.groovy'
))  

記得修改成自己的Flutter Module名稱,之后Sync一下項(xiàng)目。Binding可能會(huì)因?yàn)檎也坏蕉鴺?biāo)紅,我沒(méi)有導(dǎo)包最后也可以Sync成功,并不影響module的引入,這一點(diǎn)我還不清楚是什么原因,如果有知道的小伙伴歡迎提出。
Sync后我們可以看到項(xiàng)目中多了一個(gè)名稱為flutter的library module,我們需要在app下的build.gradle文件中添加該module的依賴。

implementation project(':flutter')

這樣就成功地將Flutter引入到了Android原生項(xiàng)目中。

Android和Flutter的交互

通過(guò)上面的幾個(gè)步驟我們已經(jīng)在Android原生項(xiàng)目中集成了Flutter,之后就需要解決交互問(wèn)題了。首先介紹一下Android頁(yè)面和Flutter頁(yè)面之間的跳轉(zhuǎn)。

Tips:由于Flutter版本的更新,下面介紹的內(nèi)容中存在一些API已經(jīng)被廢棄的情況,不過(guò)各種交互場(chǎng)景的處理思路是不變的,關(guān)于Flutter版本變更的內(nèi)容我補(bǔ)充到了文章最后,大家可以結(jié)合起來(lái)看。

Android原生頁(yè)面跳轉(zhuǎn)Flutter頁(yè)面

基本思路就是將Flutter編寫的頁(yè)面嵌入到Activity中,官方提供了兩種方式:通過(guò)FlutterViewFlutterFragment,下面我們分別看一下這兩種方式是如何實(shí)現(xiàn)的。
1.使用FlutterView
首先新建一個(gè)Activity,命名為FlutterPageActivity(名稱隨意起),在onCreate()方法中添加以下代碼:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 通過(guò)FlutterView引入Flutter編寫的頁(yè)面
    View flutterView = Flutter.createView(this, getLifecycle(), "route1");
    FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(600, 800);
    layout.leftMargin = 100;
    layout.topMargin = 200;
    addContentView(flutterView, layout);
}

Flutter.createView()方法返回的是一個(gè)FlutterView,它繼承自View,我們可以把它當(dāng)做一個(gè)普通的View,調(diào)用addContentView()方法將這個(gè)View添加到Activity的contentView中。我們注意到Flutter.createView()方法的第三個(gè)參數(shù)傳入了"route1"字符串,表示路由名稱,它確定了Flutter中要顯示的Widget,接下來(lái)需要在之前創(chuàng)建好的Flutter Module中編寫邏輯了,修改main.dart文件中的代碼:

import 'dart:ui';
import 'package:flutter/material.dart';

void main() => runApp(_widgetForRoute(window.defaultRouteName));

Widget _widgetForRoute(String route) {
  switch (route) {
    case 'route1':
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: Text('Flutter頁(yè)面'),
          ),
          body: Center(
            child: Text('Flutter頁(yè)面,route=$route'),
          ),
        ),
      );
    default:
      return Center(
        child: Text('Unknown route: $route', textDirection: TextDirection.ltr),
      );
  }
}

runApp()方法中通過(guò)window.defaultRouteName可以獲取到我們?cè)?code>Flutter.createView()方法中傳入的路由名稱,即"route1",之后編寫了一個(gè)_widgetForRoute()方法,根據(jù)傳入的route字符串顯示相應(yīng)的Widget。
最后在MainActivity中添加一個(gè)Button,編寫點(diǎn)擊事件,點(diǎn)擊Button跳轉(zhuǎn)到FlutterPageActivity。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Button btnJumpToFlutter = findViewById(R.id.btn_jump_to_flutter);
    btnJumpToFlutter.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(MainActivity.this, FlutterPageActivity.class);
            startActivity(intent);
        }
    });

運(yùn)行項(xiàng)目,點(diǎn)擊MainActivity中的Button跳轉(zhuǎn)到FlutterPageActivity,效果如下圖所示:

可以看到我們已經(jīng)成功地將Flutter編寫的Widget嵌入到了Activity中,為了更逼真一些,還需要做一些調(diào)整。首先修改LayoutParams參數(shù),將View占滿屏幕。

View flutterView = Flutter.createView(this, getLifecycle(), "route1");
FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.MATCH_PARENT);
addContentView(flutterView, layout);

然后需要隱藏原生的標(biāo)題欄,在資源文件夾res/values中的style.xml文件中添加一個(gè)FlutterPageTheme。

<style name="FlutterPageTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!--狀態(tài)欄透明-->
    <item name="android:windowTranslucentStatus">true</item>
</style>

然后在AndroidManifest.xml文件中設(shè)置Activity的Theme。

<activity
    android:name=".FlutterPageActivity"
    android:theme="@style/FlutterPageTheme" />

再次運(yùn)行項(xiàng)目看一下效果,這樣就自然多了,當(dāng)然我們還可以繼續(xù)修改標(biāo)題欄的背景顏色,這里就不提了。


2.使用FlutterFragment
為了簡(jiǎn)單,我們依然使用FlutterPageActivity,新建一個(gè)布局文件activity_flutter_page

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <FrameLayout
        android:id="@+id/fl_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

修改onCreate()方法:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_flutter_page);
    // 通過(guò)FlutterFragment引入Flutter編寫的頁(yè)面
    FragmentTransaction tx = getSupportFragmentManager().beginTransaction();
    tx.replace(R.id.fl_container, Flutter.createFragment("route1"));
    tx.commit();
}

Flutter.createFragment()方法傳入的參數(shù)同樣表示路由名稱,用于確定Flutter要顯示的Widget,返回一個(gè)FlutterFragment,該類繼承自Fragment,將該Fragment添加到Activity中就可以了。
在調(diào)試時(shí)會(huì)遇到一個(gè)問(wèn)題,顯示出Flutter頁(yè)面之前會(huì)黑屏幾秒,不要擔(dān)心,打了release包后就沒(méi)問(wèn)題了。
如何傳遞參數(shù)跳轉(zhuǎn)
通過(guò)以上兩種方式實(shí)現(xiàn)了將Flutter編寫的頁(yè)面嵌入到Activity中,但是這只是最簡(jiǎn)單的情況,如果我們需要在頁(yè)面跳轉(zhuǎn)時(shí)傳遞參數(shù)呢,如何在Flutter代碼中獲取到原生代碼中的參數(shù)呢?其實(shí)很簡(jiǎn)單,只需要在route后面拼接上參數(shù)就可以了,以創(chuàng)建FlutterView的方式為例。

View flutterView = Flutter.createView(this, getLifecycle(),
                "route1?{\"name\":\"StephenCurry\"}");

這里將路由名稱和參數(shù)間用“?”隔開(kāi),就像瀏覽器中的url一樣,參數(shù)使用了Json格式傳遞,原因就是方便Flutter端解析,而且對(duì)于一些復(fù)雜的數(shù)據(jù),比如自定義對(duì)象,使用Json序列化也很好實(shí)現(xiàn)。這時(shí)候Flutter端通過(guò)window.defaultRouteName獲取到的就是路由名稱+參數(shù)了,我們需要將路由名稱和參數(shù)分開(kāi),這就只是單純的字符串處理了,代碼如下所示:

String url = window.defaultRouteName;
// route名稱
String route =
    url.indexOf('?') == -1 ? url : url.substring(0, url.indexOf('?'));
// 參數(shù)Json字符串
String paramsJson =
    url.indexOf('?') == -1 ? '{}' : url.substring(url.indexOf('?') + 1);
// 解析參數(shù)
Map<String, dynamic> params = json.decode(paramsJson);

通過(guò)"?"將路由名稱和參數(shù)分開(kāi),將參數(shù)對(duì)應(yīng)的Json字符串解析為Map對(duì)象,需要導(dǎo)入dart:convert包,之后再將參數(shù)傳遞給對(duì)應(yīng)的Widget即可,這里就不展示了,詳細(xì)代碼可以查看Demo。運(yùn)行效果如下圖所示:

Android原生頁(yè)面跳轉(zhuǎn)Flutter頁(yè)面

Flutter頁(yè)面跳轉(zhuǎn)Android原生頁(yè)面

在實(shí)現(xiàn)Flutter頁(yè)面跳轉(zhuǎn)Android原生頁(yè)面之前首先介紹一下Platform Channel,它是Flutter和原生通信的工具,有三種類型:

  • BasicMessageChannel:用于傳遞字符串和半結(jié)構(gòu)化的信息,F(xiàn)lutter和平臺(tái)端進(jìn)行消息數(shù)據(jù)交換時(shí)候可以使用。
  • MethodChannel:用于傳遞方法調(diào)用(method invocation),F(xiàn)lutter和平臺(tái)端進(jìn)行直接方法調(diào)用時(shí)候可以使用。
  • EventChannel:用于數(shù)據(jù)流(event streams)的通信,F(xiàn)lutter和平臺(tái)端進(jìn)行事件監(jiān)聽(tīng)、取消等可以使用。

這里我就只介紹一下MethodChannel的使用,它也是我們開(kāi)發(fā)中最常用的,關(guān)于其他兩種Channel的使用可以自行查閱網(wǎng)上的文章。Flutter跳轉(zhuǎn)原生頁(yè)面就是通過(guò)MethodChannel來(lái)實(shí)現(xiàn)的,在Flutter中調(diào)用原生的跳轉(zhuǎn)方法就可以了,接下來(lái)我們具體看一下如何實(shí)現(xiàn):
1.Android端

// 定義Channel名稱
private static final String CHANNEL_NATIVE = "com.example.flutter/native";

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_flutter_page);
    // 通過(guò)FlutterView引入Flutter編寫的頁(yè)面
    FlutterView flutterView = Flutter.createView(this, getLifecycle(),
            "route1?{\"name\":\"" + getIntent().getStringExtra("name") + "\"}");
    FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT);
    addContentView(flutterView, layout);

    MethodChannel nativeChannel = new MethodChannel(flutterView, CHANNEL_NATIVE);
    nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
        @Override
        public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
            switch (methodCall.method) {
                case "jumpToNative":
                    // 跳轉(zhuǎn)原生頁(yè)面
                    Intent jumpToNativeIntent = new Intent(FlutterPageActivity.this, NativePageActivity.class);
                    jumpToNativeIntent.putExtra("name", (String) methodCall.argument("name"));
                    startActivity(jumpToNativeIntent);
                    break;
                default:
                    result.notImplemented();
                    break;
            }
        }
    });
}

首先定義Channel名稱,需要保證是唯一的,在Flutter端需要使用同樣的名稱來(lái)創(chuàng)建MethodChannel。MethodChannel的構(gòu)造方法有三個(gè)參數(shù),第一個(gè)是messenger,類型是BinaryMessenger,是一個(gè)接口,代表消息信使,是消息發(fā)送與接收的工具,由于FlutterView實(shí)現(xiàn)了BinaryMessenger,因此這里直接傳入了Flutter.createView()方法的返回值;第二個(gè)參數(shù)是name,就是Channel名稱;第三個(gè)參數(shù)是codec,類型是MethodCodec,代表消息的編解碼器,這里沒(méi)有傳該參數(shù),默認(rèn)使用StandardMethodCodec。
這里補(bǔ)充一下,如果采用FlutterFragment的方式該如何獲取到FlutterView呢,我們可以查看一下FlutterFragment的源碼。

public class FlutterFragment extends Fragment {
  public static final String ARG_ROUTE = "route";
  private String mRoute = "/";

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (getArguments() != null) {
      mRoute = getArguments().getString(ARG_ROUTE);
    }
  }

  @Override
  public void onInflate(Context context, AttributeSet attrs, Bundle savedInstanceState) {
    super.onInflate(context, attrs, savedInstanceState);
  }

  @Override
  public FlutterView onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    return Flutter.createView(getActivity(), getLifecycle(), mRoute);
  }
}

可以看到FlutterFragment的onCreateView()方法也是通過(guò)Flutter.createView()創(chuàng)建了FlutterView并返回,因此可以通過(guò)Fragment的getView()方法獲取到FlutterView。但是這里還有一個(gè)問(wèn)題,在Activity中通過(guò)Flutter.createFragment()創(chuàng)建出Fragment后再調(diào)用getView()方法獲取到的View為null,這是因?yàn)橹挥性?code>onCreateView()方法執(zhí)行完成后才會(huì)給Fragment持有的View賦值,關(guān)于這個(gè)問(wèn)題,我也沒(méi)有太好的解決方案,能想到的只是仿照FlutterFragment自定義一個(gè)Fragment,在內(nèi)部創(chuàng)建MethodChannel。

public class MyFlutterFragment extends FlutterFragment {

    private static final String CHANNEL_NATIVE = "com.example.flutter/native";

    public static MyFlutterFragment newInstance(String route) {
        MyFlutterFragment fragment = new MyFlutterFragment();
        Bundle args = new Bundle();
        args.putString(ARG_ROUTE, route);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // 這里保證了getView()返回值不為null
        MethodChannel nativeChannel = new MethodChannel((FlutterView) getView(), CHANNEL_NATIVE);
        nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
                switch (methodCall.method) {
                    case "jumpToNative":
                        // 跳轉(zhuǎn)原生頁(yè)面
                        Intent jumpToNativeIntent = new Intent(getActivity(), NativePageActivity.class);
                        jumpToNativeIntent.putExtra("name", (String) methodCall.argument("name"));
                        startActivity(jumpToNativeIntent);
                        break;
                    default:
                        result.notImplemented();
                        break;
                }
            }
        });
    }
}

創(chuàng)建FlutterFragment時(shí)使用MyFlutterFragment.newInstance()代替Flutter.createFragment(),傳入路由名稱和參數(shù)。

FragmentTransaction tx = getSupportFragmentManager().beginTransaction();
MyFlutterFragment flutterFragment = MyFlutterFragment.newInstance("route1?{\"name\":\"StephenCurry\"}");
tx.replace(R.id.fl_container, flutterFragment);
tx.commit();

這樣就解決了FlutterFragment獲取FlutterView的問(wèn)題,不過(guò)我覺(jué)得這種方案并不好,將MethodChannel定義在了Fragment中,耦合度太高,如果大家有更好的解決方案歡迎提出,目前來(lái)看我還是建議使用Flutter.createView()的方式來(lái)引入Flutter頁(yè)面。
回到正題,定義好了MethodChannel之后調(diào)用setMethodCallHandler()方法設(shè)置消息處理回調(diào),參數(shù)是MethodHandler類型,需要實(shí)現(xiàn)它的onMethodCall()方法。onMethodCall()方法有兩個(gè)參數(shù)methodCallresult,methodCall記錄了調(diào)用的方法信息,包括方法名和參數(shù),result用于方法的返回值,可以通過(guò)result.success()方法返回信息給Flutter端。之后根據(jù)方法名和參數(shù)來(lái)執(zhí)行原生的代碼就可以了,這里是跳轉(zhuǎn)到原生Activity。
2.Flutter端
在Flutter端同樣需要定義一個(gè)MethodChannel,使用MethodChannel需要引入services.dart包,Channel名稱要和Android端定義的相同。

static const nativeChannel =
    const MethodChannel('com.example.flutter/native');

在Flutter頁(yè)面中添加一個(gè)按鈕,點(diǎn)擊按鈕執(zhí)行跳轉(zhuǎn)原生頁(yè)面操作,通過(guò)調(diào)用MethodChannel的invokeMethod()方法可以執(zhí)行原生代碼,該方法有兩個(gè)參數(shù),第一個(gè)是方法名,在Android端可以通過(guò)回調(diào)方法中的methodCall.method獲取到;第二個(gè)是方法的參數(shù),可以不傳,在Android端可以通過(guò)methodCall.arguments()以及methodCall.argument()獲取到所有參數(shù)或者指定名稱的參數(shù)。

RaisedButton(
    child: Text('跳轉(zhuǎn)Android原生頁(yè)面'),
    onPressed: () {
      // 跳轉(zhuǎn)原生頁(yè)面
      Map<String, dynamic> result = {'name': 'KlayThompson'};
      nativeChannel.invokeMethod('jumpToNative', result);
    })

這里我們也注意到了,F(xiàn)lutter頁(yè)面跳轉(zhuǎn)原生頁(yè)面?zhèn)鬟f參數(shù)是通過(guò)invokeMethod()方法的第二個(gè)參數(shù)實(shí)現(xiàn)的,在Android端通過(guò)methodCall.argument()方法獲取到參數(shù)后再put到Intent里面就可以了。運(yùn)行效果如下圖所示:

Flutter頁(yè)面跳轉(zhuǎn)Android原生頁(yè)面

到這里我們已經(jīng)基本實(shí)現(xiàn)了Flutter和Android原生之間的頁(yè)面跳轉(zhuǎn)和參數(shù)傳遞,此外還有一些需要我們注意的地方。

  • 1.onActivityResult如何實(shí)現(xiàn)

在開(kāi)發(fā)中我們經(jīng)常會(huì)遇到關(guān)閉當(dāng)前頁(yè)面的同時(shí)返回給上一個(gè)頁(yè)面數(shù)據(jù)的場(chǎng)景,在Android中是通過(guò)startActivityForResultonActivityResult()實(shí)現(xiàn)的,而純Flutter頁(yè)面之間可以通過(guò)在Navigator.of(context).pop()方法中添加參數(shù)來(lái)實(shí)現(xiàn),那么對(duì)于Flutter頁(yè)面和Android原生頁(yè)面之間如何在返回上一頁(yè)時(shí)傳遞數(shù)據(jù)呢,通過(guò)MethodChannel就可以實(shí)現(xiàn)。
Flutter頁(yè)面返回Android原生頁(yè)面
這種情況直接在Flutter端調(diào)用原生的返回方法就可以了,首先在Flutter頁(yè)面添加一個(gè)按鈕,點(diǎn)擊按鈕返回原生頁(yè)面,代碼如下:

RaisedButton(
    child: Text('返回上一頁(yè)'),
    onPressed: () {
      // 返回給上一頁(yè)的數(shù)據(jù)
      Map<String, dynamic> result = {'message': '我從Flutter頁(yè)面回來(lái)了'};
      nativeChannel.invokeMethod('goBackWithResult', result);
    }),

Android端依然是通過(guò)判斷methodCall.method的值來(lái)執(zhí)行指定的代碼,通過(guò)methodCall.argument()獲取Flutter傳遞的參數(shù)。

nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
    @Override
    public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
        switch (methodCall.method) {
            case "goBackWithResult":
                // 返回上一頁(yè),攜帶數(shù)據(jù)
                Intent backIntent = new Intent();
                backIntent.putExtra("message", (String) methodCall.argument("message"));
                setResult(RESULT_OK, backIntent);
                finish();
                break;
        }
    }
});

之后在上一個(gè)Activity的onActivityResult()方法中編寫邏輯就可以了,這里就不展示了。
Android原生頁(yè)面返回Flutter頁(yè)面
與上一種情況不同的是,這種情況需要原生來(lái)調(diào)用Flutter代碼,和Flutter調(diào)用原生方法的步驟是一樣的,我們來(lái)具體看一下。首先在Flutter跳轉(zhuǎn)到的頁(yè)面NativePageActivity中添加一個(gè)按鈕,點(diǎn)擊按鈕返回Flutter頁(yè)面,并傳遞數(shù)據(jù)。

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_native_page);

    Button btnBack = findViewById(R.id.btn_back);
    btnBack.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent();
            intent.putExtra("message", "我從原生頁(yè)面回來(lái)了");
            setResult(RESULT_OK, intent);
            finish();
        }
    });
}

然后修改一下Flutter跳轉(zhuǎn)原生頁(yè)面的代碼,將startActivity改為startActivityForResult,并重寫onActivityResult()方法,在方法內(nèi)部獲取到原生頁(yè)面返回的數(shù)據(jù),創(chuàng)建MethodChannel,調(diào)用invokeMethod()方法將數(shù)據(jù)傳遞給Flutter端,這里定義的方法名為"onActivityResult"。

private static final String CHANNEL_FLUTTER = "com.example.flutter/flutter";

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode) {
        case 0:
            if (data != null) {
                // NativePageActivity返回的數(shù)據(jù)
                String message = data.getStringExtra("message");
                Map<String, Object> result = new HashMap<>();
                result.put("message", message);
                // 創(chuàng)建MethodChannel,這里的flutterView即Flutter.createView所返回的View
                MethodChannel flutterChannel = new MethodChannel(flutterView, CHANNEL_FLUTTER);
                // 調(diào)用Flutter端定義的方法
                flutterChannel.invokeMethod("onActivityResult", result);
            }
            break;
        default:
            break;
    }
}

接下來(lái)需要在Flutter端定義MethodChannel和回調(diào)方法,同樣是根據(jù)MethodCall.method的值來(lái)執(zhí)行相應(yīng)代碼,通過(guò)MethodCall.arguments來(lái)獲取參數(shù)。

static const flutterChannel =
    const MethodChannel('com.example.flutter/flutter');

@override
void initState() {
  super.initState();
  Future<dynamic> handler(MethodCall call) async {
    switch (call.method) {
      case 'onActivityResult':
        // 獲取原生頁(yè)面?zhèn)鬟f的參數(shù)
        print(call.arguments['message']);
        break;
    }
  }

  flutterChannel.setMethodCallHandler(handler);
}

這樣就實(shí)現(xiàn)了原生頁(yè)面返回Flutter頁(yè)面并返回?cái)?shù)據(jù)的場(chǎng)景,獲取到數(shù)據(jù)后就可以為所欲為啦。

  • 2.Flutter棧管理

看到這里不知道大家是否和我有相同的感受,在原生頁(yè)面(Activity)中引入Flutter頁(yè)面有些類似于Android開(kāi)發(fā)中使用WebView加載url,每個(gè)Flutter頁(yè)面對(duì)應(yīng)著一個(gè)route(url),那么我們自然就會(huì)想到一個(gè)問(wèn)題:如果在Flutter頁(yè)面中繼續(xù)跳轉(zhuǎn)到其他Flutter頁(yè)面,這時(shí)候點(diǎn)擊手機(jī)的返回鍵是否會(huì)直接返回到上一個(gè)Activity,而不是返回上一個(gè)Flutter頁(yè)面呢,通過(guò)測(cè)試發(fā)現(xiàn)確實(shí)是這樣。

那么應(yīng)該如何解決這個(gè)問(wèn)題呢,我的實(shí)現(xiàn)思路是在Flutter端利用Navigator.canPop(context)方法判斷是否可以返回上一頁(yè),如果可以就調(diào)用Navigator.of(context).pop()返回,反之則說(shuō)明當(dāng)前顯示的Flutter頁(yè)面已經(jīng)是第一個(gè)頁(yè)面了,直接返回上一個(gè)Activity即可。至于如何返回上一個(gè)Activity,當(dāng)然還是要使用MethodChannel了。既然明確了思路,我們就來(lái)看看具體實(shí)現(xiàn)吧。
首先在Flutter頁(yè)面中添加一個(gè)按鈕,點(diǎn)擊按鈕跳轉(zhuǎn)到一個(gè)新的Flutter頁(yè)面,這里的SecondPage是我新建的一個(gè)頁(yè)面,可以隨意修改,重點(diǎn)不在頁(yè)面本身,就不展示出來(lái)了。

RaisedButton(
    child: Text('跳轉(zhuǎn)Flutter頁(yè)面'),
    onPressed: () {
      Navigator.of(context)
          .push(MaterialPageRoute(builder: (context) {
        return SecondPage();
      }));
    }),

然后定義MethodChannel和MethodCallHandler回調(diào),這里的邏輯是是調(diào)用Navigator.canPop(context)判斷是否可以返回上一頁(yè),如果可以就調(diào)用Flutter自身的返回上一頁(yè)方法,如果已經(jīng)是第一個(gè)Flutter頁(yè)面了就調(diào)用原生方法返回上一個(gè)Activity,即這里的nativeChannel.invokeMethod('goBack')。

static const nativeChannel =
    const MethodChannel('com.example.flutter/native');
static const flutterChannel =
    const MethodChannel('com.example.flutter/flutter');

@override
void initState() {
  super.initState();
  Future<dynamic> handler(MethodCall call) async {
    switch (call.method) {
      case 'goBack':
        // 返回上一頁(yè)
        if (Navigator.canPop(context)) {
          Navigator.of(context).pop();
        } else {
          nativeChannel.invokeMethod('goBack');
        }
        break;
    }
  }

  flutterChannel.setMethodCallHandler(handler);
}

接下來(lái)我們?cè)賮?lái)看Android端,首先需要重寫onBackPressed()方法,將返回鍵的事件處理交給Flutter端。

private static final String CHANNEL_FLUTTER = "com.example.flutter/flutter";

@Override
public void onBackPressed() {
    MethodChannel flutterChannel = new MethodChannel(flutterView, CHANNEL_FLUTTER);
    flutterChannel.invokeMethod("goBack", null);
}

最后編寫原生端的MethodCallHandler回調(diào),如果當(dāng)前Flutter頁(yè)面是第一個(gè)時(shí)調(diào)用該方法直接finish掉Activity。

nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
switch (methodCall.method) {
    case "goBack":
        // 返回上一頁(yè)
        finish();
        break;
    default:
        result.notImplemented();
        break;
}

現(xiàn)在我們?cè)賮?lái)看看運(yùn)行效果,這樣就很舒服了。

Flutter升級(jí)到1.12后遇到的問(wèn)題

前些日子評(píng)論區(qū)里wangwhatlh同學(xué)反饋
遇到了程序包io.flutter.facade不存在問(wèn)題,起初我運(yùn)行了一下之前的項(xiàng)目,發(fā)現(xiàn)可以正常運(yùn)行,加上我自己有一段時(shí)間沒(méi)有用過(guò)Flutter了,也就沒(méi)太重視這個(gè)問(wèn)題。說(shuō)來(lái)也是慚愧,最近又陸續(xù)有多位小伙伴反饋了這個(gè)問(wèn)題,我才終于意識(shí)到這是一個(gè)普遍性問(wèn)題,簡(jiǎn)單查了一下了解到這個(gè)錯(cuò)誤是Flutter 1.12版本廢棄了io.flutter.facade包導(dǎo)致的,我自己更新了Flutter版本后重新運(yùn)行項(xiàng)目也遇到了這個(gè)問(wèn)題,所以要對(duì)此前遇到這個(gè)問(wèn)題的大家說(shuō)聲抱歉,確實(shí)是我沒(méi)有重視這個(gè)問(wèn)題,之后對(duì)于大家提出的問(wèn)題我一定盡快反饋。
好了,接下來(lái)就介紹一下解決方案吧,首先附上官方的一些相關(guān)說(shuō)明文檔,大家可以自行閱讀一下文檔,文檔中介紹的還是比較詳細(xì)的。
Upgrading pre 1.12 Android projects
Experimental: Add Flutter View
Add Flutter to existing app
下面進(jìn)入正題,簡(jiǎn)單介紹一下在Flutter 1.12版本中幾個(gè)需要修改的地方。

  • 原生頁(yè)面中引入Flutter

上文在介紹Android原生頁(yè)面跳轉(zhuǎn)Flutter頁(yè)面時(shí)提到了兩種方案:FlutterView和FlutterFragment,我們來(lái)分別看一下現(xiàn)在應(yīng)該如何實(shí)現(xiàn)。
首先是通過(guò)FlutterView引入Flutter頁(yè)面,以前我們是通過(guò)io.flutter.facade包中Flutter類的createView()方法創(chuàng)建出一個(gè)FlutterView,然后添加到Activity的布局中,但是由于io.flutter.facade包的廢棄,該方法已經(jīng)無(wú)法使用。官方的文檔有說(shuō)明目前不提供在View級(jí)別引入Flutter的便捷API,因此如果可能的話,我們應(yīng)該避免使用FlutterView,但是通過(guò)FlutterView引入Flutter頁(yè)面也是可行的,代碼如下:

// 通過(guò)FlutterView引入Flutter編寫的頁(yè)面
FlutterView flutterView = new FlutterView(this);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.MATCH_PARENT);
FrameLayout flContainer = findViewById(R.id.fl_container);
flContainer.addView(flutterView, lp);
// 關(guān)鍵代碼,將Flutter頁(yè)面顯示到FlutterView中
flutterView.attachToFlutterEngine(flutterEngine);

需要注意,這里的FlutterView位于io.flutter.embedding.android包中,和此前我們所創(chuàng)建的FlutterView(位于io.flutter.view包中)是不一樣的。我們通過(guò)查看FlutterView的源碼可以發(fā)現(xiàn)它繼承自FrameLayout,因此像一個(gè)普通的View那樣添加就可以了。接下來(lái)的這一步很關(guān)鍵,調(diào)用FlutterView的attachToFlutterEngine()方法,這個(gè)方法的作用就是將Flutter編寫的UI頁(yè)面顯示到FlutterView中,我們注意到這里傳入了一個(gè)flutterEngine參數(shù),它又是什么呢?flutterEngine的類型為FlutterEngine,字面意思就是Flutter引擎,它負(fù)責(zé)在Android端執(zhí)行Dart代碼,將Flutter編寫的UI顯示到FlutterView/FlutterActivity/FlutterFragment中。創(chuàng)建FlutterEngine的代碼如下:

FlutterEngine flutterEngine = new FlutterEngine(this);
flutterEngine.getDartExecutor().executeDartEntrypoint(
        DartExecutor.DartEntrypoint.createDefault()
);

這樣就創(chuàng)建好了一個(gè)FlutterEngine對(duì)象,默認(rèn)情況下FlutterEngine加載的路由名稱為"/",我們可以通過(guò)下面的代碼指定初始路由名稱:

flutterEngine.getNavigationChannel().setInitialRoute("route1");

至于傳參的情況沒(méi)有變化,直接在路由名稱后面拼接參數(shù)就可以了。當(dāng)然,F(xiàn)lutterView也可以直接在xml布局文件中添加,最后同樣需要調(diào)用attachToFlutterEngine()方法將Flutter編寫的UI頁(yè)面顯示到FlutterView中,這里就不展示了。
補(bǔ)充一下,最近我將Flutter版本更新到了1.17,發(fā)現(xiàn)上述代碼運(yùn)行后FlutterView無(wú)法顯示,和官方提供的示例flutter_view進(jìn)行了對(duì)比,才發(fā)現(xiàn)缺少了下面的代碼:

@Override
protected void onResume() {
    super.onResume();
    flutterEngine.getLifecycleChannel().appIsResumed();
}

@Override
protected void onPause() {
    super.onPause();
    flutterEngine.getLifecycleChannel().appIsInactive();
}

@Override
protected void onStop() {
    super.onStop();
    flutterEngine.getLifecycleChannel().appIsPaused();
}

相信大家都能看出這和生命周期有關(guān),flutterEngine.getLifecycleChannel()獲取到的是一個(gè)LifecycleChannel對(duì)象,類比于MethodChannel,作用大概就是將Flutter和原生端的生命周期相互聯(lián)系起來(lái)。這里分別在onResume()、onPause()onStop()方法中調(diào)用了LifecycleChannel的appIsResumed()、appIsInactive()appIsPaused()方法,作用就是同步Flutter端與原生端的生命周期。添加上述代碼后,F(xiàn)lutterView就可以正常顯示了。至于為什么在Flutter 1.17版本(也有可能是更早的版本)中需要添加上述代碼,我猜想可能是FlutterVIew的渲染機(jī)制有了一些變化,在接收到原生端對(duì)應(yīng)生命周期方法中發(fā)送的通知才會(huì)顯示,具體原理我也不是很清楚,如果有說(shuō)得不對(duì)的地方或是大家有了解這部分內(nèi)容的歡迎提出。
然后是通過(guò)FlutterFragment引入Flutter頁(yè)面,我們此前是通過(guò)Flutter.createFragment()方法創(chuàng)建出FlutterFragment,現(xiàn)在同樣無(wú)法使用了。官方提供了三種創(chuàng)建FlutterFragment的方式,我們來(lái)分別看一下。
方式一、FlutterFragment.createDefault()

// 通過(guò)FlutterFragment引入Flutter編寫的頁(yè)面
FlutterFragment flutterFragment = FlutterFragment.createDefault();
getSupportFragmentManager()
        .beginTransaction()
        .add(R.id.fl_container, flutterFragment)
        .commit();

通過(guò)FlutterFragment.createDefault()創(chuàng)建出FlutterFragment,需要注意這里的FlutterFragment位于io.flutter.embedding.android包中,和我們此前使用的FlutterFragment不是同一個(gè)類。創(chuàng)建好之后就沒(méi)什么可說(shuō)的了,按照正常的Fragment添加就好。createDefault()方法創(chuàng)建出的Fragment顯示的路由名稱為"/",如果我們需要指定其他路由名稱就不能使用這個(gè)方法了。
方式二、FlutterFragment.withNewEngine()

// 通過(guò)FlutterFragment引入Flutter編寫的頁(yè)面
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
        .initialRoute("route1")
        .build();
getSupportFragmentManager()
        .beginTransaction()
        .add(R.id.fl_container, flutterFragment)
        .commit();

通過(guò)FlutterFragment.withNewEngine()獲取到NewEngineFragmentBuilder對(duì)象,使用建造者模式構(gòu)造出FlutterFragment對(duì)象,可以通過(guò)initialRoute()方法指定初始路由名稱。同樣地,傳遞參數(shù)只需要在路由名稱后面進(jìn)行拼接。
方式三、FlutterFragment.withCachedEngine

// 創(chuàng)建可緩存的FlutterEngine對(duì)象
FlutterEngine flutterEngine = new FlutterEngine(this);
flutterEngine.getNavigationChannel().setInitialRoute("route1");
flutterEngine.getDartExecutor().executeDartEntrypoint(
        DartExecutor.DartEntrypoint.createDefault()
);
FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine);

// 通過(guò)FlutterFragment引入Flutter編寫的頁(yè)面
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
        .build();

方式二使用的withNewEngine()方法從名稱上也能看出每次都是創(chuàng)建一個(gè)新的FlutterEngine對(duì)象來(lái)顯示Flutter UI,但是從官方文檔中我們可以了解到每個(gè)FlutterEngine對(duì)象在顯示出Flutter UI之前是需要一個(gè)warm-up(不知道能不能翻譯為預(yù)熱)期的,這會(huì)導(dǎo)致屏幕呈現(xiàn)短暫的空白,解決方式就是預(yù)先創(chuàng)建并啟動(dòng)FlutterEngine,完成warm-up過(guò)程,然后將這個(gè)FlutterEngine緩存起來(lái),之后使用這個(gè)FlutterEngine來(lái)顯示出Flutter UI。上面的代碼中執(zhí)行的FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine)就是將FlutterEngine緩存起來(lái),這里傳入的"my_engine_id"就相當(dāng)于緩存名稱。之后通過(guò)FlutterFragment.withCachedEngine()方法來(lái)創(chuàng)建FlutterFragment,參數(shù)傳入上面的緩存名稱。需要注意,withCachedEngine()方法返回的是一個(gè)CachedEngineFragmentBuilder對(duì)象,同樣是使用了建造者模式,但是它是沒(méi)有initialRoute()方法的,如果我們要指定初始路由,需要在創(chuàng)建FlutterEngine對(duì)象時(shí)通過(guò)setInitialRoute()方法來(lái)設(shè)置。
除此之外,F(xiàn)lutter 1.12中還提供了一種原生引入Flutter頁(yè)面方式——使用FlutterActivity,這里的FlutterActivity也是位于io.flutter.embedding.android包下的。下面我簡(jiǎn)單介紹一下如何通過(guò)FlutterActivity引入Flutter編寫的UI,大家也可以參考官網(wǎng)的介紹。
首先需要在AndroidManifest.xml文件中注冊(cè)FlutterActivity,代碼如下:

<activity
    android:name="io.flutter.embedding.android.FlutterActivity"
    android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
    android:hardwareAccelerated="true"
    android:theme="@style/AppTheme"
    android:windowSoftInputMode="adjustResize" />

這里的theme可以替換為自己項(xiàng)目中定義的主題。注冊(cè)好FlutterActivity后,第二步就是直接啟動(dòng)這個(gè)Activity了,啟動(dòng)FlutterActivity有以下三種方式:

// 方式一、FutterActivity顯示的路由名稱為"/",不可設(shè)置
startActivity(
        FlutterActivity.createDefaultIntent(this)
);

// 方式二、FutterActivity顯示的路由名稱可設(shè)置,每次都創(chuàng)建一個(gè)新的FlutterEngine對(duì)象
startActivity(
        FlutterActivity
                .withNewEngine()
                .initialRoute("route1")
                .build(this)
);

// 方式三、FutterActivity顯示的路由名稱可設(shè)置,使用緩存好的FlutterEngine對(duì)象
startActivity(
        FlutterActivity
                .withCachedEngine("my_engine_id")
                .build(this)
);

是不是很熟悉,和上面介紹的創(chuàng)建FlutterFragment的三種方式是對(duì)應(yīng)的,這里我就不再介紹了。與通過(guò)FlutterView/FlutterFragment引入Flutter UI不同,這種方式不需要我們自己創(chuàng)建一個(gè)Activity,F(xiàn)lutterActivity顯示的Flutter路由是在創(chuàng)建Intent對(duì)象時(shí)指定的,優(yōu)點(diǎn)就是使用起來(lái)更簡(jiǎn)單,缺點(diǎn)就是不夠靈活,無(wú)法像FlutterView/FlutterFragment那樣只是作為原生頁(yè)面中的一部分展示,因此這種方式更適合整個(gè)頁(yè)面都是由Flutter編寫的場(chǎng)景。
在調(diào)試階段,跳轉(zhuǎn)FlutterActivity之后也會(huì)黑屏幾秒,同樣地,打了release包后就沒(méi)有問(wèn)題了。

  • Android原生與Flutter交互

交互這塊的變化不大,還是使用MethodChannel來(lái)進(jìn)行方法的調(diào)用,但是在Android端創(chuàng)建MethodChannel時(shí)需要注意了,我們此前都是傳入io.flutter.view包下的FlutterView作為BinaryMessenger,現(xiàn)在肯定是無(wú)法獲取到該類對(duì)象了,那么這個(gè)參數(shù)應(yīng)該傳什么呢。通過(guò)查看繼承關(guān)系我們可以找到兩個(gè)相關(guān)的類:DartExecutorDartMessenger。DartExecutor可以通過(guò)FlutterEngine的getDartExecutor()方法獲得,而DartMessenger又可以通過(guò)DartExecutor的getBinaryMessenger()方法獲得,因此我們可以這樣創(chuàng)建MethodChannel:

MethodChannel nativeChannel = new MethodChannel(flutterEngine.getDartExecutor(), "com.example.flutter/native");
// 或
MethodChannel nativeChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "com.example.flutter/native");

至于這兩種方式使用哪個(gè),我目前是沒(méi)發(fā)現(xiàn)有什么表現(xiàn)上的區(qū)別,如果哪位大佬有了解過(guò)可以分享一下。
此外,我們還需要注意,這里在創(chuàng)建MethodChannel時(shí)傳入的FlutterEngine對(duì)象必須和我們此前創(chuàng)建好的FlutterView/FlutterFragment中使用的是同一個(gè)。拿FlutterFragment舉例,上面介紹了三種創(chuàng)建FlutterFragment的方式,第三種是使用已經(jīng)創(chuàng)建好的FlutterEngine對(duì)象,我們可以直接傳入這個(gè)FlutterEngine對(duì)象。但是前兩種方式都是會(huì)創(chuàng)建出一個(gè)新的FlutterEngine,我們?nèi)绾潍@取到FlutterEngine對(duì)象呢?FlutterFragment中定義了一個(gè)getFlutterEngine()方法,從方法名來(lái)看大概就是獲取FlutterEngine對(duì)象。我嘗試過(guò)創(chuàng)建MethodChannel時(shí)傳入flutterFragment.getFlutterEngine().getDartExecutor(),運(yùn)行后會(huì)直接拋出空指針異常,異常產(chǎn)生的位置在FlutterFragment的getFlutterEngine()方法中:

@Nullable
public FlutterEngine getFlutterEngine() {
    return delegate.getFlutterEngine();
}

錯(cuò)誤原因是這里的delegate為null,全局搜索一下,發(fā)現(xiàn)在FlutterFragment的onAttach()方法中會(huì)對(duì)delegate賦值,也就是說(shuō)明此時(shí)沒(méi)有執(zhí)行onAttach()方法。我猜測(cè)這就是由于上面提到過(guò)的FlutterEngine的warm-up機(jī)制,這是一個(gè)耗時(shí)過(guò)程,因此FlutterFragment并不會(huì)立刻執(zhí)行onAttach()方法,導(dǎo)致我們?cè)贏ctivity的onCreate()方法中直接使用FlutterFragment的getFlutterEngine()方法會(huì)拋出異常。目前我也沒(méi)想到有什么解決方案,如果我們要利用FlutterFragment來(lái)進(jìn)行交互,還是只能使用withCachedEngine()方法來(lái)創(chuàng)建FlutterFragment,在構(gòu)造MethodChannel時(shí)傳入創(chuàng)建好的FlutterEngine對(duì)象。
到這里Flutter 1.12中關(guān)于原生交互的幾個(gè)變更基本上就介紹得差不多了,相關(guān)代碼我也已經(jīng)更新。最后還是要感嘆一下Flutter的更新速度,才幾個(gè)月沒(méi)看就變化這么大,之后的版本可能還會(huì)修改,如果我了解到有什么變更會(huì)及時(shí)更新文章,大家有什么發(fā)現(xiàn)也歡迎提出。

關(guān)于AndroidX

現(xiàn)在越來(lái)越多的Android項(xiàng)目都使用了AndroidX庫(kù),之前寫這篇文章時(shí)由于還是使用的support庫(kù),因此這里簡(jiǎn)單介紹一下AndroidX的遷移。
關(guān)于Android原生項(xiàng)目的遷移我就不介紹了,相信大家接觸過(guò)Android開(kāi)發(fā)的都有了解過(guò),網(wǎng)上也有很多相關(guān)文章。在Flutter版本1.12.13之后,新建的Module默認(rèn)就是使用AndroidX庫(kù)的,那么如何把現(xiàn)有的Flutter Module遷移到AndroidX呢,其實(shí)很簡(jiǎn)單,在Flutter Module根目錄下的pubspec.yaml文件中添加下面的配置就可以了:

module:
  androidX: true // Add this line.

添加之后在命令行執(zhí)行flutter clean命令即可,執(zhí)行完成后我們打開(kāi).android(或android)目錄下的gradle.properties文件,會(huì)看到添加了如下配置,這就說(shuō)明Flutter Module已經(jīng)成功遷移到了AndroidX庫(kù)。

當(dāng)我們把Flutter Module和Android項(xiàng)目都遷移到AndroidX庫(kù)之后會(huì)發(fā)現(xiàn)代碼中有報(bào)錯(cuò):

可以看到是在調(diào)用FragmentTransaction的add()方法時(shí)報(bào)的錯(cuò),報(bào)錯(cuò)的原因就是因?yàn)檫@里傳入的參數(shù)類型(flutterFragment類型)不正確,我們可以點(diǎn)開(kāi)FlutterFragment類看一下,發(fā)現(xiàn)在類文件中定義的FlutterFragment類還是繼承自support包下的Fragment,那么這里當(dāng)然就會(huì)報(bào)錯(cuò)了。先別慌,當(dāng)你嘗試運(yùn)行項(xiàng)目時(shí)會(huì)發(fā)現(xiàn)可以正常運(yùn)行,這是為什么呢?其實(shí)一開(kāi)始我也有寫疑惑,后來(lái)仔細(xì)想了想大概是android.enableJetifier=true這個(gè)配置的作用,上面也見(jiàn)到過(guò),是項(xiàng)目遷移到AndroidX庫(kù)后自動(dòng)添加的,大家可能知道它的作用是將項(xiàng)目中的第三方庫(kù)自動(dòng)遷移到AndroidX庫(kù),因此雖然我們從源碼中看到的FlutterFragment類還是使用的support包,但是gardle在編譯時(shí)已經(jīng)自動(dòng)將FlutterFragment類遷移到了AndroidX庫(kù),所以運(yùn)行時(shí)不會(huì)報(bào)錯(cuò)。
關(guān)于AndroidX庫(kù)遷移的具體內(nèi)容大家也可以查看官方文檔AndroidX Migration。
目前最新版本(1.18.0)的Flutter中,F(xiàn)lutterFragment已經(jīng)改為繼承自androidx包下的Fragment了,編輯器也不會(huì)報(bào)錯(cuò)了,不過(guò)具體是哪個(gè)版本修改的我就不清楚了,如果大家有知道的歡迎提出。

總結(jié)

本文介紹了Android項(xiàng)目中引入Flutter的方法以及簡(jiǎn)單交互場(chǎng)景的實(shí)現(xiàn)。
1.Android項(xiàng)目引入Flutter本質(zhì)上是將Flutter編寫的Widget嵌入到Activity中,類似于WebView,容器Activity相當(dāng)于WebView,route相當(dāng)于url,有兩種方式FlutterView和FlutterFragment。頁(yè)面間的跳轉(zhuǎn)和傳參可以借助MethodChannel來(lái)實(shí)現(xiàn)。
2.關(guān)于MethodChannel,它的作用是Flutter和原生方法的互相調(diào)用,使用時(shí)在兩端都要定義MethodChannel,通過(guò)相同的name聯(lián)系起來(lái),調(diào)用方使用invokeMethod(),傳入方法名和參數(shù);被調(diào)用方定義MethodCallHandler回調(diào),根據(jù)方法名和方法參數(shù)執(zhí)行相應(yīng)的平臺(tái)代碼。
3.本文中所提到的一些方案可能并不是最好的,如果大家有自己的見(jiàn)解歡迎一起交流學(xué)習(xí)。此外,文中的一些代碼只展示了部分,Demo我已經(jīng)上傳到了github,大家如果需要的話可以查看。
4.最后提一下flutter_boost,這是閑魚團(tuán)隊(duì)開(kāi)源的一個(gè)Flutter混合開(kāi)發(fā)插件,我簡(jiǎn)單地嘗試了一下,還是挺好用的,在頁(yè)面跳轉(zhuǎn)和傳參方面都很方便,大家感興趣的話可以了解一下。

參考文章

Add Flutter to existing app
Flutter混編:在Android原生中混編Flutter
flutter接入現(xiàn)有的app詳細(xì)介紹

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

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