Android 混合Flutter之原始碼整合方式

真丶深紅騎士發表於2019-08-26

一、前言

Flutter自從1.0版本釋出,現在越來越受歡迎,很多公司都在研究或者用在專案上。今天實踐一下Android原生專案如何巢狀Flutter頁面,具體原理就是Flutter作為Android Module出現在專案中,這樣就可以在已有的專案中使用,Android專案也是一個工程,Flutter專案也是一個工程,這樣就互不相關,也很好進行管理。廢話不多說,開始實踐。

二、目錄結構

首先講一下整個工程的結構:

工程專案結構示意圖

1.建立Android工程

在建立Android工程前,新建一個資料夾(目錄),取名叫:mixProject,裡面在建立兩個資料夾分別是:flutter和native,示意圖如下:注意:後面flutter資料夾會刪除,這裡這是說明整個工程的目錄

資料夾目錄
下面就在native資料夾建立Android工程,File->New->New Project:

建立Android工程

2.建立Module模式的Flutter工程

建立工程之前先把flutte資料夾目錄刪除,在mixProject目錄下以Module形式建立Flutter工程,File->New->New Flutter Project,這裡要注意,選型別的要選Flutter Module,Flutter專案跟Android工程根資料夾是同級的,它不同於普通的Android module存在於Android工程根目錄下。

module形式建立Flutter

module形式建立Flutter(二)

這樣Android工程和Flutter工程都已經建立好了。 另外也可以通過(在專案根目錄下)命令flutter create -t module my_flutter去建立Flutter的Module工程。

3.設定新增Flutter Module

下面在Android工程下新增對Flutter工程的依賴,在專案根目錄下setting.gradle新增如下:

//insert
setBinding(new Binding([gradle: this]))                                 // new
evaluate(new File(                                                      // new
        settingsDir.parentFile,                                               // new
        'my_flutter/.android/include_flutter.groovy'                          // new
))
複製程式碼

Android工程新增對Flutter的依賴
這樣要注意:xxxx/.android/include_flutter.groovy中的xxxx一定要和以module形式建立的Flutter工程名一致。 這時候Sync一下,發現Flutter的module已經新增到專案中了。

4.新增工程依賴

在Android工程app下的build.gradle下對Flutter的依賴:

//加入Flutter的依賴
implementation project(':flutter')
複製程式碼

這時候在同步一下,如果沒報錯,證明flutter工程已經依賴進Android工程裡了,如果出現下面錯誤:

出現錯誤
flutter工程和Android工程下minSdkVersion要一致。

三、原始碼簡單分析

1. FlutterActivity

Android原生呼叫Flutter頁面之前,先知道FlutterActivity這個類,在建立的FlutterModule.android->app->flutter_module->host下有個MainActivity,這個類是繼承FlutterActivity類,在AndroidManifest.xml下並且配置了這個啟動介面,也就是說當原生Android呼叫Flutter時,該類是Flutter專案的頁面入口。那麼下面看看這個類的原始碼,到底做了什麼?

FlutterActivity實現的介面
可以發現它繼承了Activity,也就是它還是普通的Activity,另外還實現了三個介面:

  • Provider

這個介面只有一個方法:

    public interface Provider {
        FlutterView getFlutterView();
    }
複製程式碼

只是返回當前Activity中的FlutterView

  • PluginRegistry
public interface PluginRegistry {
    //註冊外掛
    PluginRegistry.Registrar registrarFor(String var1);
    //是否有這個外掛
    boolean hasPlugin(String var1);
    //外掛釋出值
    <T> T valuePublishedByPlugin(String var1);
    //為外掛註冊生命回撥
    public interface PluginRegistrantCallback {
        void registerWith(PluginRegistry var1);
    }
    //檢視銷燬監聽
    public interface ViewDestroyListener {
        boolean onViewDestroy(FlutterNativeView var1);
    }
    //使用者手動離開當前activity監聽,如主動切換任何,按back健
    //系統自動切換應用不會呼叫此方法,如來電,滅屏
    public interface UserLeaveHintListener {
        void onUserLeaveHint();
    }
    //監聽Activity是否執行onNewIntent的回撥
    public interface NewIntentListener {
        boolean onNewIntent(Intent var1);
    }
    //監聽Activity是否執行onActivityResult
    public interface ActivityResultListener {
        boolean onActivityResult(int var1, int var2, Intent var3);
    }
    //監聽Activity是否請求許可權的回撥
    public interface RequestPermissionsResultListener {
        boolean onRequestPermissionsResult(int var1, String[] var2, int[] var3);
    }

    //外掛的註冊者
    public interface Registrar {
        //外掛宿主的activity
        Activity activity();
        //外掛的上下文 Application Context
        Context context();
        //這是當前Activity的context
        Context activeContext();
        //信使 主要用來註冊Platform channels
        BinaryMessenger messenger();
        //返回TextureRegistry 可以拿到SurfaceTexture
        TextureRegistry textures();
        //返回PlatformViewRegistry
        PlatformViewRegistry platformViewRegistry();
        //返回FlutterView
        FlutterView view();
        //根據key來尋找資源
        String lookupKeyForAsset(String var1);
        //同理根據key來尋找資源
        String lookupKeyForAsset(String var1, String var2);
        //釋出值
        PluginRegistry.Registrar publish(Object var1);
        //增加回撥
        PluginRegistry.Registrar addRequestPermissionsResultListener(PluginRegistry.RequestPermissionsResultListener var1);
        //增加回撥
        PluginRegistry.Registrar addActivityResultListener(PluginRegistry.ActivityResultListener var1);
        //增加回撥newIntent回撥
        PluginRegistry.Registrar addNewIntentListener(PluginRegistry.NewIntentListener var1);
        //增加回撥
        PluginRegistry.Registrar addUserLeaveHintListener(PluginRegistry.UserLeaveHintListener var1);
        //增加回撥檢視銷燬
        PluginRegistry.Registrar addViewDestroyListener(PluginRegistry.ViewDestroyListener var1);
    }
}

複製程式碼
  • ViewFactory
    //檢視工廠
    public interface ViewFactory {
        //建立FlutterView
        FlutterView createFlutterView(Context var1);
        //建立FlutterNativeView
        FlutterNativeView createFlutterNativeView();
        //是否保留FlutterNativeView
        boolean retainFlutterNativeView();
    }
複製程式碼

也就是FlutterActivity實現上面三個介面主要是建立檢視,返回檢視以及監聽生命週期的回撥。下面回到FlutterActivityFLutterActivityDelegate後面再分析:

    //建立委託類FlutterActivityDelegate物件
    private final FlutterActivityDelegate delegate = new FlutterActivityDelegate(this, this);
    private final FlutterActivityEvents eventDelegate;
    private final Provider viewProvider;
    private final PluginRegistry pluginRegistry;
    //建構函式
    public FlutterActivity() {
        //FlutterActivityDelegate實現了FlutterActivityEvents,Provider,PluginRegistry 賦值對應的變數,呼叫更加清晰
        this.eventDelegate = this.delegate;
        this.viewProvider = this.delegate;
        this.pluginRegistry = this.delegate;
    }
複製程式碼

並且Activity的生命週期函式都是由FlutterActivityEvents物件來執行:

  protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.eventDelegate.onCreate(savedInstanceState);
    }

    protected void onStart() {
        super.onStart();
        this.eventDelegate.onStart();
    }

    protected void onResume() {
        super.onResume();
        this.eventDelegate.onResume();
    }

    protected void onDestroy() {
        this.eventDelegate.onDestroy();
        super.onDestroy();
    }

    public void onBackPressed() {
        if (!this.eventDelegate.onBackPressed()) {
            super.onBackPressed();
        }

    }

    protected void onStop() {
        this.eventDelegate.onStop();
        super.onStop();
    }

    protected void onPause() {
        super.onPause();
        this.eventDelegate.onPause();
    }

    protected void onPostResume() {
        super.onPostResume();
        this.eventDelegate.onPostResume();
    }

    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        this.eventDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (!this.eventDelegate.onActivityResult(requestCode, resultCode, data)) {
            super.onActivityResult(requestCode, resultCode, data);
        }

    }

    protected void onNewIntent(Intent intent) {
        this.eventDelegate.onNewIntent(intent);
    }

    public void onUserLeaveHint() {
        this.eventDelegate.onUserLeaveHint();
    }

    public void onTrimMemory(int level) {
        this.eventDelegate.onTrimMemory(level);
    }

    public void onLowMemory() {
        this.eventDelegate.onLowMemory();
    }

    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        this.eventDelegate.onConfigurationChanged(newConfig);
    }
複製程式碼

下面看看建立FlutterView以及返回FlutterView的方法:

    public FlutterView getFlutterView() {
        //通過FlutterActivityDelegate委託執行
        return this.viewProvider.getFlutterView();
    }
    //子類實現 返回null
    public FlutterView createFlutterView(Context context) {
        return null;
    }
    //子類實現 返回null
    public FlutterNativeView createFlutterNativeView() {
        return null;
    }
複製程式碼

外掛管理的方法實現:

    public final boolean hasPlugin(String key) {
        //也是通過FlutterActivityDelegate委託執行
        return this.pluginRegistry.hasPlugin(key);
    }

    public final <T> T valuePublishedByPlugin(String pluginKey) {
        return this.pluginRegistry.valuePublishedByPlugin(pluginKey);
    }

    public final Registrar registrarFor(String pluginKey) {
        return this.pluginRegistry.registrarFor(pluginKey);
    }
複製程式碼

那麼這裡很清晰地知道FlutterActivity的生命週期各個方法實際由FlutterActivityDelegate代理執行,並且知道FlutterActivity通過委託代理的方式解決來生命週期的回撥,外掛管理和FlutterView的建立,是Android原生調Flutter頁面的中間橋樑。

2. FlutterActivityDelegate

經過上面的分析,FlutterActivityDelegate作為委託的角色存在,下面更進一步地去深入:

    public FlutterActivityDelegate(Activity activity, FlutterActivityDelegate.ViewFactory viewFactory) {
        this.activity = (Activity)Preconditions.checkNotNull(activity);
        this.viewFactory = (FlutterActivityDelegate.ViewFactory)Preconditions.checkNotNull(viewFactory);
    }
複製程式碼

FlutterActivityDelegate建構函式需要傳入Activity物件和FlutterActivityDelegate.ViewFactory,其實重點看Activity物件就行,因為傳遞給委託類FlutterActivityDelegateViewFactory並沒有生成FlutterView,恰好相反,FlutterView是通過傳遞進來的Activity來生成的。在FlutterActivityDelegate類原始碼可以看到,定義類和Activity同名的函式,如:onCreate,onPause,onStart,onResume。在FlutterActivity中呼叫這個委託類同名函式,因此得出Flutter頁面是由該委託類處理的。下面具體看一下onCreate方法:

    public void onCreate(Bundle savedInstanceState) {
        if (VERSION.SDK_INT >= 21) {
            Window window = this.activity.getWindow();
            window.addFlags(-2147483648);
            window.setStatusBarColor(1073741824);
            window.getDecorView().setSystemUiVisibility(1280);
        }
        //獲取啟動引數
        String[] args = getArgsFromIntent(this.activity.getIntent());
        //保證FlutterMain初始化完成
        FlutterMain.ensureInitializationComplete(this.activity.getApplicationContext(), args);
        //注意這裡,在FlutterActivity預設返回null的
        this.flutterView = this.viewFactory.createFlutterView(this.activity);
        //所以會走到這裡
        if (this.flutterView == null) {
            //這裡也是建立類空FlutterNativeView
            FlutterNativeView nativeView = this.viewFactory.createFlutterNativeView();
            //這裡才是實際建立了FlutterView
            this.flutterView = new FlutterView(this.activity, (AttributeSet)null, nativeView);
            //設定佈局引數,新增到當前activity,作為主檢視
            this.flutterView.setLayoutParams(matchParent);
            this.activity.setContentView(this.flutterView);
            //建立啟動ui
            this.launchView = this.createLaunchView();
            if (this.launchView != null) {
                this.addLaunchView();
            }
        }
        //根據activity獲取intent中傳遞的路由值
        if (!this.loadIntent(this.activity.getIntent())) {
            //獲取路由值 去跳轉flutter專案設定的route對應頁面
            //查詢bundle
            String appBundlePath = FlutterMain.findAppBundlePath(this.activity.getApplicationContext());
            if (appBundlePath != null) {
                this.runBundle(appBundlePath);
            }

        }
    }
複製程式碼

上面的步驟就是:

  • 根據當前系統版本來設定沉浸式狀態列;
  • 獲取開啟Activity時通過intent傳入的引數資訊;
  • 執行FlutterMain的ensureInitializationComplete方法;
  • 建立FlutterNativeView;
  • 根據FlutterNativeView建立FlutterView;
  • 將FlutterView設定為activity的內容檢視;
  • 通過FlutterMain查詢appBundle所在路徑,並執行appBundle; 從上面可以得知,FlutterActivityDelegate這個類的onCreate方法主要是建立FlutterView並且設定到Activity上,然後通過loadIntent方法去讀取intent中傳遞的路由值去跳轉到Flutter專案中對應的頁面去。

3.FlutterView

上面講述道Activity會將FlutterView設定到setContView裡,下面簡單看看FlutterView原始碼:

public class FlutterView extends SurfaceView implements BinaryMessenger, TextureRegistry
複製程式碼

看到FlutterView繼承了SurfaceView,至於為什麼要繼承SurfaceView,因為SurfaceView使用的繪圖執行緒不是UI執行緒,平時需要圖形效能比較高的場景就得需要它了。

public class FlutterView extends SurfaceView implements BinaryMessenger, TextureRegistry {
    private final NavigationChannel navigationChannel;//重點看這個
    private final KeyEventChannel keyEventChannel;
    private final LifecycleChannel lifecycleChannel;
    private final LocalizationChannel localizationChannel;
    //建構函式
    public FlutterView(Context context) {
        this(context, (AttributeSet)null);
    }

    public FlutterView(Context context, AttributeSet attrs) {
        this(context, attrs, (FlutterNativeView)null);
    }

    public FlutterView(Context context, AttributeSet attrs, FlutterNativeView nativeView) {
        super(context, attrs);
        this.nextTextureId = new AtomicLong(0L);
        this.mIsSoftwareRenderingEnabled = false;
        this.onAccessibilityChangeListener = new OnAccessibilityChangeListener() {
            public void onAccessibilityChanged(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) {
                FlutterView.this.resetWillNotDraw(isAccessibilityEnabled, isTouchExplorationEnabled);
            }
        };
        Activity activity = getActivity(this.getContext());
        if (activity == null) {
            throw new IllegalArgumentException("Bad context");
        } else {
            //如果傳遞的FlutterNativeView是空
            if (nativeView == null) {
                //重新建立預設的FlutterNativeView
                this.mNativeView = new FlutterNativeView(activity.getApplicationContext());
            } else {
                this.mNativeView = nativeView;
            }

            this.dartExecutor = this.mNativeView.getDartExecutor();
            this.flutterRenderer = new FlutterRenderer(this.mNativeView.getFlutterJNI());
            this.mIsSoftwareRenderingEnabled = FlutterJNI.nativeGetIsSoftwareRenderingEnabled();
            //適配視窗變化,並在合適的時候更新mMetrics,設定到native中
            this.mMetrics = new FlutterView.ViewportMetrics();
            this.mMetrics.devicePixelRatio = context.getResources().getDisplayMetrics().density;

        }
    }
}
複製程式碼

下面重點觀察NavigationChannel這個導航Channel:

public class NavigationChannel {
    @NonNull
    public final MethodChannel channel;

    public NavigationChannel(@NonNull DartExecutor dartExecutor) {
        //建立MethodChannel
        this.channel = new MethodChannel(dartExecutor, "flutter/navigation", JSONMethodCodec.INSTANCE);
    }
    //設定初始路由
    public void setInitialRoute(String initialRoute) {
        this.channel.invokeMethod("setInitialRoute", initialRoute);
    }
    //將指定路由壓入棧
    public void pushRoute(String route) {
        this.channel.invokeMethod("pushRoute", route);
    }
    //將指定路由彈出棧
    public void popRoute() {
        this.channel.invokeMethod("popRoute", (Object)null);
    }
    //設定MethodCallHandler
    public void setMethodCallHandler(@Nullable MethodCallHandler handler) {
        this.channel.setMethodCallHandler(handler);
    }
}
複製程式碼

也就是說FlutterView導航是通過MethodChannelFlutter進行通訊,最終交由Flutter處理。做個外掛都知道,在Flutter肯定存在MethodChannel('flutter/navigation',JSONMethodCodec),在ststem_channels.dart中找到:

  /// A JSON [MethodChannel] for navigation.
  ///
  /// The following incoming methods are defined for this channel (registered
  /// using [MethodChannel.setMethodCallHandler]):
  ///
  ///  * `popRoute`, which is called when the system wants the current route to
  ///    be removed (e.g. if the user hits a system-level back button).
  ///
  ///  * `pushRoute`, which is called with a single string argument when the
  ///    operating system instructs the application to open a particular page.
  ///
  /// See also:
  ///
  ///  * [WidgetsBindingObserver.didPopRoute] and
  ///    [WidgetsBindingObserver.didPushRoute], which expose this channel's
  ///    methods.
  static const MethodChannel navigation = MethodChannel(
      'flutter/navigation',
      JSONMethodCodec(),
  );
複製程式碼

並且在widgets/binding.dart找到對應實現:

  Future<dynamic> _handleNavigationInvocation(MethodCall methodCall) {
    switch (methodCall.method) {
      case 'popRoute':
        //壓入棧
        return handlePopRoute();
      case 'pushRoute':
        //出棧
        return handlePushRoute(methodCall.arguments);
    }
    return Future<dynamic>.value();
  }
複製程式碼

但是沒有看到setInitialRoute處理,那麼在哪裡會用到呢?在app.dart下:

 /// The [MaterialApp] configures the top-level [Navigator] to search for routes
 /// in the following order:
 ///
 ///  1. For the `/` route, the [home] property, if non-null, is used.
 ///
 ///  2. Otherwise, the [routes] table is used, if it has an entry for the route.
 ///
 ///  3. Otherwise, [onGenerateRoute] is called, if provided. It should return a
 ///     non-null value for any _valid_ route not handled by [home] and [routes].
 ///
 ///  4. Finally if all else fails [onUnknownRoute] is called.
 ///
 /// If a [Navigator] is created, at least one of these options must handle the
 /// `/` route, since it is used when an invalid [initialRoute] is specified on
 /// startup (e.g. by another application launching this one with an intent on
 /// Android; see [Window.defaultRouteName]).
 ///
 /// This widget also configures the observer of the top-level [Navigator] (if
 /// any) to perform [Hero] animations.
 ///
 /// If [home], [routes], [onGenerateRoute], and [onUnknownRoute] are all null,
 /// and [builder] is not null, then no [Navigator] is created.
 /// {@macro flutter.widgets.widgetsApp.initialRoute}
  final String initialRoute;
複製程式碼

上面說明了Natvigator配置尋找路由順序:

  • 1.對於"/"路由,如果[home]屬性不為空,則會使用
  • 2.否則,將會使用路由表(如果有路由條目)
  • 3.否則,將會呼叫[onGenerateRoute]提供一個有效沒有被[home]和[routes]處理的路由
  • 4.最後,如果前面尋找失敗,則呼叫[onUnknownRoute] 再檢視widgetsApp下具體說明:
  /// {@template flutter.widgets.widgetsApp.initialRoute}
  /// The name of the first route to show, if a [Navigator] is built.
  ///
  /// Defaults to [Window.defaultRouteName], which may be overridden by the code
  /// that launched the application.
  ///
  /// If the route contains slashes, then it is treated as a "deep link", and
  /// before this route is pushed, the routes leading to this one are pushed
  /// also. For example, if the route was `/a/b/c`, then the app would start
  /// with the three routes `/a`, `/a/b`, and `/a/b/c` loaded, in that order.
  ///
  /// If any part of this process fails to generate routes, then the
  /// [initialRoute] is ignored and [Navigator.defaultRouteName] is used instead
  /// (`/`). This can happen if the app is started with an intent that specifies
  /// a non-existent route.
  /// The [Navigator] is only built if routes are provided (either via [home],
  /// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
  /// [initialRoute] must be null and [builder] must not be null.
  ///
  /// See also:
  ///
  ///  * [Navigator.initialRoute], which is used to implement this property.
  ///  * [Navigator.push], for pushing additional routes.
  ///  * [Navigator.pop], for removing a route from the stack.
  /// {@endtemplate}
  final String initialRoute;
複製程式碼

如果生成了[navigator],則initialRoute是第一個展示的預設路由,預設是Window.defaultRouteName,而在window.dartdefaultName更進一步的說明:

  /// The route or path that the embedder requested when the application was
  /// launched.
  ///
  /// This will be the string "`/`" if no particular route was requested.
  ///
  /// ## Android
  ///
  /// On Android, calling
  /// [`FlutterView.setInitialRoute`](/javadoc/io/flutter/view/FlutterView.html#setInitialRoute-java.lang.String-)
  /// will set this value. The value must be set sufficiently early, i.e. before
  /// the [runApp] call is executed in Dart, for this to have any effect on the
  /// framework. The `createFlutterView` method in your `FlutterActivity`
  /// subclass is a suitable time to set the value. The application's
  /// `AndroidManifest.xml` file must also be updated to have a suitable
  /// [`<intent-filter>`](https://developer.android.com/guide/topics/manifest/intent-filter-element.html).
  ///
  /// ## iOS
  ///
  /// On iOS, calling
  /// [`FlutterViewController.setInitialRoute`](/objcdoc/Classes/FlutterViewController.html#/c:objc%28cs%29FlutterViewController%28im%29setInitialRoute:)
  /// will set this value. The value must be set sufficiently early, i.e. before
  /// the [runApp] call is executed in Dart, for this to have any effect on the
  /// framework. The `application:didFinishLaunchingWithOptions:` method is a
  /// suitable time to set this value.
  ///
  /// See also:
  ///
  ///  * [Navigator], a widget that handles routing.
  ///  * [SystemChannels.navigation], which handles subsequent navigation
  ///    requests from the embedder.
  String get defaultRouteName => _defaultRouteName();
  String _defaultRouteName() native 'Window_defaultRouteName';
複製程式碼

註釋的意思如果沒有特定的路由,預設是**/AndroidIOS**如何設定該值方式和時機,再回到FlutterView裡:

    public void setInitialRoute(String route) {
        this.navigationChannel.setInitialRoute(route);
    }
複製程式碼

到這裡,已經清楚Flutter如何接受native傳遞的路由引數過程了。就是通過FlutterView可以設定該路由值,在native建立FlutterView並且通過setInitialRoute方法設定route(window.defaultRouteName),而Flutter通過window.defaultRouteName從而知道native要跳轉到Flutter專案的哪個頁面。 再回到FlutterView的建構函式中,或者大家和我可能會有疑惑:為什麼要建立FlutterNativeView呢?那下面簡單看看FlutterNativeView的原始碼:

4.FlutterNativeView

public class FlutterNativeView implements BinaryMessenger {
    private static final String TAG = "FlutterNativeView";
    //外掛管理
    private final FlutterPluginRegistry mPluginRegistry;
    private final DartExecutor dartExecutor;
    private FlutterView mFlutterView;
    private final FlutterJNI mFlutterJNI;
    private final Context mContext;
    private boolean applicationIsRunning;

    public FlutterNativeView(@NonNull Context context) {
        this(context, false);
    }

    public FlutterNativeView(@NonNull Context context, boolean isBackgroundView) {
        this.mContext = context;
        this.mPluginRegistry = new FlutterPluginRegistry(this, context);
        //建立FlutterJNI
        this.mFlutterJNI = new FlutterJNI();
        this.mFlutterJNI.setRenderSurface(new FlutterNativeView.RenderSurfaceImpl());
        this.dartExecutor = new DartExecutor(this.mFlutterJNI);
        this.mFlutterJNI.addEngineLifecycleListener(new FlutterNativeView.EngineLifecycleListenerImpl());
        this.attach(this, isBackgroundView);
        this.assertAttached();
    }
}
複製程式碼

可以看到FlutterNativeView實現了BinaryMessenger介面,根據其意思可以得知,這個BinaryMessenger是一個資料資訊交流物件,介面宣告如下:

public interface BinaryMessenger {
    void send(String var1, ByteBuffer var2);

    void send(String var1, ByteBuffer var2, BinaryMessenger.BinaryReply var3);

    void setMessageHandler(String var1, BinaryMessenger.BinaryMessageHandler var2);

    public interface BinaryReply {
        void reply(ByteBuffer var1);
    }

    public interface BinaryMessageHandler {
        void onMessage(ByteBuffer var1, BinaryMessenger.BinaryReply var2);
    }
}
複製程式碼

這是用於FlutterNative之間交換資料的介面類,已知FlutterView已經實現了SurfaceView,而FlutterNativeView負責FlutterViewFlutter之間的通訊,再使用Skia繪製頁面。

下面再看看FlutterJNI這個類:

public class FlutterJNI {
    ...
    public FlutterJNI() {
    }
    private native void nativeDestroy(long var1);
    
    private native long nativeAttach(FlutterJNI var1, boolean var2);
        private static native void nativeDetach(long var0);

    private static native void nativeRunBundleAndSnapshot(long var0, String var2, String var3, String var4, boolean var5, AssetManager var6);

    private static native void nativeRunBundleAndSource(long var0, String var2, String var3, String var4);

    private static native void nativeSetAssetBundlePathOnUI(long var0, String var2);

    private static native String nativeGetObservatoryUri();

    private static native void nativeDispatchEmptyPlatformMessage(long var0, String var2, int var3);

    private static native void nativeDispatchPlatformMessage(long var0, String var2, ByteBuffer var3, int var4, int var5);
}
複製程式碼

發現涉及到很多和native打交道的方法,可以知道NativeView顯然是一個外掛、訊息的管理類,並與native打交道,那麼和FlutterView的關係,顯然一個負責展示,一個負責互動。

5.loadIntent

在上面分析FlutterActivity實現了getFlutterView方法,也分析到在FlutterActivityDelegate建立了FlutterView並新增到當前Activity中。當FlutterView被新增到Activity,那麼Flutter怎麼知道native開啟哪個頁面呢,其實是通過loadIntent這個方法來開啟對應的頁面,下面具體看看這個再FlutterActivityDelegate這個類裡的loadIntent方法:

        //根據activity獲取intent中傳遞的路由值
        if (!this.loadIntent(this.activity.getIntent())) {
            String appBundlePath = FlutterMain.findAppBundlePath(this.activity.getApplicationContext());
            if (appBundlePath != null) {
                this.runBundle(appBundlePath);
            }

        }
        
        .....
        
        private boolean loadIntent(Intent intent) {
        String action = intent.getAction();
        if ("android.intent.action.RUN".equals(action)) {
            String route = intent.getStringExtra("route");
            String appBundlePath = intent.getDataString();
            if (appBundlePath == null) {
               //查詢bundle
                appBundlePath = FlutterMain.findAppBundlePath(this.activity.getApplicationContext());
            }

            if (route != null) {
                //flutterView初始化,引數為路由
                this.flutterView.setInitialRoute(route);
            }

            this.runBundle(appBundlePath);
            return true;
        } else {
            return false;
        }
    }

複製程式碼

6.runBundle

    //runBundle方法
    private void runBundle(String appBundlePath) {
    //第一次啟動flutter頁面isApplicationRunning()為false
    if (!this.flutterView.getFlutterNativeView().isApplicationRunning()) {
        FlutterRunArguments args = new FlutterRunArguments();
        ArrayList<String> bundlePaths = new ArrayList();
        //檢查是否有flutter相關資源,這裡用於動態更新
        ResourceUpdater resourceUpdater = FlutterMain.getResourceUpdater();
        if (resourceUpdater != null) {
            File patchFile = resourceUpdater.getInstalledPatch();
            JSONObject manifest = resourceUpdater.readManifest(patchFile);
            if (resourceUpdater.validateManifest(manifest)) {
                bundlePaths.add(patchFile.getPath());
            }
        }
        //設定對應的執行引數
        bundlePaths.add(appBundlePath);
        args.bundlePaths = (String[])bundlePaths.toArray(new String[0]);
        
        args.entrypoint = "main";
        //通過flutterView.runFromBundle()來執行
        this.flutterView.runFromBundle(args);
    }

}
複製程式碼

可以看到最後通過FlutterViewrunFromBundle()執行。

7.runFromBundle

    public void runFromBundle(FlutterRunArguments args) {
        this.assertAttached();
        this.preRun();
        this.mNativeView.runFromBundle(args);
        this.postRun();
    }
複製程式碼

呼叫FlutterNativeViewrunFromBundle方法:

    public void runFromBundle(FlutterRunArguments args) {
        boolean hasBundlePaths = args.bundlePaths != null && args.bundlePaths.length != 0;
        if (args.bundlePath == null && !hasBundlePaths) {
            throw new AssertionError("Either bundlePath or bundlePaths must be specified");
        } else if ((args.bundlePath != null || args.defaultPath != null) && hasBundlePaths) {
            throw new AssertionError("Can't specify both bundlePath and bundlePaths");
        } else if (args.entrypoint == null) {
            throw new AssertionError("An entrypoint must be specified");
        } else {
            if (hasBundlePaths) {
                this.runFromBundleInternal(args.bundlePaths, args.entrypoint, args.libraryPath);
            } else {
                this.runFromBundleInternal(new String[]{args.bundlePath, args.defaultPath}, args.entrypoint, args.libraryPath);
            }

        }
    }
複製程式碼

當Bundle引數不為空的時候,呼叫runFromBundleInternal方法:

    private void runFromBundleInternal(String[] bundlePaths, String entrypoint, String libraryPath) {
        this.assertAttached();
        if (this.applicationIsRunning) {
            throw new AssertionError("This Flutter engine instance is already running an application");
        } else {
            this.mFlutterJNI.runBundleAndSnapshotFromLibrary(bundlePaths, entrypoint, libraryPath, this.mContext.getResources().getAssets());
            this.applicationIsRunning = true;
        }
    }
複製程式碼

最後通過FlutterJNI來呼叫JNI方法執行:

    @UiThread
    public void runBundleAndSnapshotFromLibrary(@NonNull String[] prioritizedBundlePaths, @Nullable String entrypointFunctionName, @Nullable String pathToEntrypointFunction, @NonNull AssetManager assetManager) {
        this.ensureAttachedToNative();
        this.nativeRunBundleAndSnapshotFromLibrary(this.nativePlatformViewId, prioritizedBundlePaths, entrypointFunctionName, pathToEntrypointFunction, assetManager);
    }
複製程式碼
  • nativePlatformViewId 這是在FlutterView建立FlutterNativeView,FlutteNativeView呼叫FlutterJNI的natvieAttach傳遞給c++層,而從c++層返回,通過這個值來執行c++的一些方法。
  • prioritizedBundlePaths,陣列中只有一個值類似/data/data/包名/flutter/flutter_assets/的路徑值,這就是路由值。
  • entrypointFunctionName:這個值為main
  • pathToEntrypointFunction:這是null

最後呼叫c++方法將main函式調起,之後就執行widget繫結,UI渲染等。這裡發現nativeRunBundleAndSnapshotFromLibrary需要傳四個引數。

這裡可以得出,只要開啟FlutterActivity頁面的時候,通過intent傳入的key,如果這個值於Flutter專案定義的route值一樣,就能跳到對應的頁面。下面用一張圖簡單描述流程:

啟動過程
也就是當原生開啟Flutter頁面的時候,其實還是跳轉Activity,只不過這個Activity鋪了FlutterView來顯示,那下面具體實踐。

四、簡單例子

1.搭建頁面

這邊例子只有主頁面(Activity),主頁面由一個ViewPager和底部RadioGroup組成:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >


    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/view_line"/>

    <View
        android:id="@+id/view_line"
        android:layout_width="match_parent"
        android:layout_height="2dp"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/rl_bottom_radio"
        android:background="#ece7e7"/>




    <RelativeLayout
        android:id="@+id/rl_bottom_radio"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent"
     >


        <RadioGroup
            android:id="@+id/rg_foot_bar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

            <RadioButton
                android:id="@+id/radio_button_one"
                style="@style/main_footer_bar_radio"
                android:checked="true"
                android:drawableTop="@drawable/widget_bar_one"
                android:text="跳轉到flutter"
                />
            <RadioButton
                android:="@+id/radio_button_two"
                style="@style/main_footer_bar_radio"

                android:drawableTop="@drawable/widget_bar_two"
                android:text="測試"
                />
            <RadioButton
                android:="@+id/radio_button_three"
                style="@style/main_footer_bar_radio"
                android:drawableTop="@drawable/widget_bar_three"
                android:text="網路"
                />

            
        </RadioGroup>
        
    </RelativeLayout>

</android.support.constraint.ConstraintLayout>
複製程式碼

ViewPager分別由三個Fragment組成,分別是跳到Flutter頁面,測試頁面和網路載入Flutter頁面。 MainActivity:

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


    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"


        />

    <View
        android:id="@+id/view_line"
        android:layout_width="match_parent"
        android:layout_height="2dp"
        android:background="#ece7e7" />


    <RelativeLayout
        android:id="@+id/rl_bottom_radio"
        android:layout_width="match_parent"
        android:layout_height="60dp"

        >

        <RadioGroup
            android:id="@+id/rg_foot_bar"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:orientation="horizontal">

            <RadioButton
                android:id="@+id/radio_button_one"
                style="@style/main_footer_bar_radio"
                android:checked="true"
                android:drawableTop="@drawable/widget_bar_one"
                android:text="跳轉到flutter" />

            <RadioButton
                android:id="@+id/radio_button_two"
                style="@style/main_footer_bar_radio"
                android:drawableTop="@drawable/widget_bar_two"
                android:text="第二個頁面" />

            <RadioButton
                android:id="@+id/radio_button_three"
                style="@style/main_footer_bar_radio"
                android:drawableTop="@drawable/widget_bar_three"
                android:text="巢狀flutter頁面" />


        </RadioGroup>

    </RelativeLayout>

</LinearLayout>
複製程式碼

2.通過Fragment巢狀Flutter

2.1.跳轉到預設頁面

io.flutter.facade下自動生成了FlutterFragment

/**
 * A {@link Fragment} managing a {@link FlutterView}.
 *
 * <p><strong>Warning:</strong> This file is auto-generated by Flutter tooling.
 * DO NOT EDIT.</p>
 */
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);
  }
}
複製程式碼

繼續點選Flutter.createView方法:



/** 
   * 建立一個連結(FlutterVIew)將指定Activity和生命週期連結起來
   * 可選初始路由字串用於確定顯示哪個小部件,預設的初始路由是“/”
   *
   * Creates a {@link FlutterView} linked to the specified {@link Activity} and {@link Lifecycle}.
   * The optional initial route string will be made available to the Dart code (via
   * {@code window.defaultRouteName}) and may be used to determine which widget should be displayed
   * in the view. The default initialRoute is "/".
   *
   * @param activity an {@link Activity}
   * @param lifecycle a {@link Lifecycle}
   * @param initialRoute an initial route {@link String}, or null
   * @return a {@link FlutterView}
   */
  @NonNull
  public static FlutterView createView(@NonNull final Activity activity, @NonNull final Lifecycle lifecycle, final String initialRoute) {
    FlutterMain.startInitialization(activity.getApplicationContext());
    FlutterMain.ensureInitializationComplete(activity.getApplicationContext(), null);
    final FlutterNativeView nativeView = new FlutterNativeView(activity);
    final FlutterView flutterView = new FlutterView(activity, null, nativeView) {
      private final BasicMessageChannel<String> lifecycleMessages = new BasicMessageChannel<>(this, "flutter/lifecycle", StringCodec.INSTANCE);
      @Override
      public void onFirstFrame() {
        super.onFirstFrame();
        setAlpha(1.0f);
      }

      @Override
      public void onPostResume() {
        // Overriding default behavior to avoid dictating system UI via PlatformPlugin.
        lifecycleMessages.send("AppLifecycleState.resumed");
      }
    };
    if (initialRoute != null) {
      flutterView.setInitialRoute(initialRoute);
    }
    lifecycle.addObserver(new LifecycleObserver() {
      @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
      public void onCreate() {
        final FlutterRunArguments arguments = new FlutterRunArguments();
        arguments.bundlePath = FlutterMain.findAppBundlePath(activity.getApplicationContext());
        arguments.entrypoint = "main";
        flutterView.runFromBundle(arguments);
        GeneratedPluginRegistrant.registerWith(flutterView.getPluginRegistry());
      }

      @OnLifecycleEvent(Lifecycle.Event.ON_START)
      public void onStart() {
        flutterView.onStart();
      }

      @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
      public void onResume() {
        flutterView.onPostResume();
      }

      @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
      public void onPause() {
        flutterView.onPause();
      }

      @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
      public void onStop() {
        flutterView.onStop();
      }

      @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
      public void onDestroy() {
        flutterView.destroy();
      }
    });
    flutterView.setAlpha(0.0f);
    return flutterView;
  }
複製程式碼

看到這個Flutter.createView(getActivity),getLifecycle(),mRoute這行程式碼已經幫我們初始了FlutterMainFlutterNativeView,FlutterView,並且返回FlutterView,那現在可以思考,那是不是建立這個系統生成的fragment就能巢狀Flutter頁面了?實踐一下:

    private void initListener(){
        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }
            //這裡的邏輯是如果點選
            @Override
            public void onPageSelected(int position) {
               //如果點選第三個RadioButton
               if(position == 2){
                   //如果沒有初始化就初始化FlutterFragment
                   if(isFirstInitFlutterView){
                       initFlutterFragment();
                       isFirstInitFlutterView = false;
                   }

               }
            }

            @Override
            public void onPageScrollStateChanged(int i) {

            }
        });
    }
    /**
     *
     * 初始化FlutterFragment
     *
     */
    private void initFlutterFragment(){
        mFragmentAdapter.updateFragment(2,new FlutterFragment());
        //更新Fragment
        mFragmentAdapter.notifyDataSetChanged();
    }
複製程式碼

看看效果圖:

巢狀Flutter頁面
可以看到只通過new FlutterFragment程式碼即可把Flutter頁面巢狀到原生Android裡。

2.2.跳轉到指定頁面

可以發現上面跳到Flutter專案的主頁面(預設頁面),下面通過指定的引數跳到指定頁面

2.2.1.建立新的Fragment

為了方便,下面自己實現FlutterFragment:

public class MyFlutterFragment extends Fragment {
    private static final String TAG = "MyFlutterFragment";
    //路由
    public static final String AGR_ROUTE = "_route_";
    //引數
    public static final String PARAMS = "_params_";
    private String mRoute = "/";
    private String mParams = "";
    private FlutterView mFlutterView;


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

    }


    @Override
    public void onCreate(@Nullable Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        if(getArguments() != null){
            mRoute = getArguments().getString(AGR_ROUTE);
            mParams = getArguments().getString(PARAMS);
            //這裡拼接引數
            JSONObject jsonObject = new JSONObject();
            JSONObject pageParamsObject;

            if(!TextUtils.isEmpty(mParams)){
                try {
                    //json字串
                    pageParamsObject = new JSONObject(mParams);
                    jsonObject.put("pageParams",pageParamsObject);
                    mRoute = mRoute + "?" + jsonObject.toString();
                    Log.d("ssd",mRoute);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }



        }
    }


    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState){
        Log.d(TAG,"onCreateView-mRoute:"+mRoute);
        mFlutterView = Flutter.createView(getActivity(),getLifecycle(),mRoute);
        //綜合解決閃屏,佈局覆蓋問題
        mFlutterView.setZOrderOnTop(true);
        mFlutterView.setZOrderMediaOverlay(false);
        mFlutterView.getHolder().setFormat(Color.parseColor("#00000000"));

        //註冊channel
       // GeneratedPluginRegistrant.registerWith(mFlutterView.getPluginRegistry());
        //返回FlutterView
        return mFlutterView;
    }
}
複製程式碼

2.2.2.在Flutter頁面處理路由引數

先把main.dart檔案程式碼所有程式碼刪除,我這裡把它作為引數解析和路由跳轉:

import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:io';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter_module/ui/tab_fragment.dart';

void main(){
  //接受路由引數 路由引數可以自定義規則 
  //window.defaultRouteName 就是獲取native傳遞的路由引數
  runApp(_widgetForRoute(window.defaultRouteName));

  //runApp(_widgetForRoute("tab_fragment"));
//
//  if(Platform.isAndroid){
//    //Android同步沉浸式
//    SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.transparent);
//    SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
//
//  }


}


/**
 * 路由引數處理
 * 
 */
Widget _widgetForRoute(String route){
  print("route:" + route.toString());
  //解析路由引數
  String pageName = _getPageName(route);
  Map<String,dynamic> pageParams = json.decode(_parseNativeParams(route));
  //路由引數
  print("pageName:" + pageName.toString());
  //業務引數
  print("pageParams:" + pageParams.toString());

  //擷取跳轉到哪個頁面引數
  switch(pageName){
    case 'tab_fragment':
       return new TabFragment();

  }


}


/**
 * 解析路由引數
 * 
 */
String _getPageName(String route){
  String pageName = route;
  if (route.indexOf("?") != -1)
      //擷取?之前的字串 表明後面帶有業務引數
      pageName = route.substring(0,route.indexOf("?"));
  print("pageName:" + pageName);
  return pageName;
}

/**
 * 返回業務引數
 * 
 */
String _parseNativeParams(String route){
  Map<String,dynamic> nativeParams = {};
  if(route.indexOf("?") != -1){
    nativeParams = json.decode(route.substring(route.indexOf("?") + 1));
  }
  return nativeParams['pageParams'] ?? "{}";

}
複製程式碼

main.dart主要是解析路由引數和業務,傳參的規則是可以自定義的,我這邊傳參是路由+業務引數,由上面看到Flutter通過window.defaultRouteName得到Android原生所傳遞的引數。上面通過_widgetForRoute方法來跳轉原生傳遞給Flutter的引數對應頁面,上面例子原生傳遞了tab_fragment,在_widgetForRoute會進入**case 'tab_fragment'**條件裡,最後跳到tab_fragment中:

2.2.3.Flutter中tab_fragment

tab_fragment主要是用dio網路庫來做一個請求網路功能

import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:flutter_module/util/http_util.dart';


/**
 * 原生Fragment巢狀Flutter
 *
 */
class TabFragment extends StatefulWidget {
   String content = "Tab3";
   @override
   _TabFragmentState createState() => _TabFragmentState();
}



class _TabFragmentState extends State<TabFragment>{

  String text = "這是預設內容";

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



  @override
  Widget build(BuildContext context){
    return MaterialApp(
      home: new Scaffold(
          body: new Container(
            color: Colors.greenAccent,
            child:new ListView(
              children: <Widget>[
                new Padding(padding:EdgeInsets.only(top:200)),
                new Container(
                  alignment: Alignment.center,
                  child: new Text(
                    widget.content,
                    style: new TextStyle(
                      color:Colors.white,
                      fontSize: 40,
                      fontWeight: FontWeight.normal,
                      decoration: TextDecoration.none
                    ),
                  ),
                ),

                new Padding(padding: EdgeInsets.only(top:200)),
                new Container(
                  width: 100,
                  alignment: Alignment.center,
                  child:new MaterialButton(
                    child:new Text("網路1請求測試"),
                    color: Colors.greenAccent,
                    onPressed: (){
                        //網路請求模擬
                        buttonClick();
                    },

                  )
                ),
                new Container(
                  alignment: Alignment.center,
                  child: new Text(
                    text,
                    style: new TextStyle(
                      color: Colors.white,
                      fontSize: 20,
                      fontWeight: FontWeight.normal,
                      decoration: TextDecoration.none
                    ),
                  ),
                ),
              ],
            ),
          ),
      ),
    );
  }

  /**
   * 點選網路請求
   *
   */
  void buttonClick() async {
    Response response = await HttpUtil().doGet("api/test");
    if(response != null){
      if(response.statusCode == 200){
        setState(() {
          print("請求成功-response:"+response.data.toString());
          text = response.data.toString();
        });
      } else {
        print("請求失敗,請檢查網路後重試");
      }

    } else {
      print("請求失敗,請檢查網路後重試");
    }


  }

}
複製程式碼

最後呼叫:

    /**
     *
     * 初始化FlutterFragment
     *
     */
    private void initFlutterFragment(){
        mFragmentAdapter.updateFragment(2,MyFlutterFragment.newInstance("tab_fragment","ssssss"));
        //更新Fragment
        mFragmentAdapter.notifyDataSetChanged();
    }
複製程式碼

效果圖如下:

fragment巢狀Flutter
點選巢狀Flutter頁面,返現Flutter頁面Fragment形式巢狀在原生中了。

3.以FlutterActivity為載體

3.1 繼承FlutterActivity

上面分析過,可以通過FlutterActivity來直接跳到Flutter頁面,並從FlutterActivityDelegate原始碼我們可按照以下幾個步驟來實現:

  • 獲取開啟Activity時通過intent傳入的引數資訊;
  • 執行FlutterMain的ensureInitializationComplete方法;
  • 建立FlutterNativeView;
  • 根據FlutterNativeView建立FlutterView;
  • 將FlutterView設定為activity的內容檢視;
/**
 *
 * Android ->Flutter(FlutterActivity為載體)
 */
public class FlutterMainActivity extends FlutterActivity implements MethodChannel.MethodCallHandler{
    private static final String TAG = "FlutterMainActivity";
    private String routeStr = "";
    private static final String TOAST_CHANNEL = "com.test.native_flutter/toast";



    @Override
    protected void onCreate(Bundle savedInstance){
       super.onCreate(savedInstance);
       //執行FlutterMain初始化
       FlutterMain.startInitialization(getApplicationContext());
       //外掛註冊
       GeneratedPluginRegistrant.registerWith(this);
       registerCustomPlugin(this);
    }


    private void registerCustomPlugin(PluginRegistry register){
         registerMethodChannel();
    }


    private void registerMethodChannel(){
        //呼叫原生toast
        new MethodChannel(this.registrarFor(TOAST_CHANNEL).messenger(),TOAST_CHANNEL);
    }
    @Override
    public FlutterView createFlutterView(Context context){
        getIntentData();
        WindowManager.LayoutParams matchParent = new WindowManager.LayoutParams(-1, -1);
        //建立FlutterNativeView
        FlutterNativeView nativeView = this.createFlutterNativeView();
        //建立FlutterView
        FlutterView flutterView = new FlutterView(FlutterMainActivity.this,(AttributeSet)null,nativeView);
        //給FlutterView傳遞路由引數
        flutterView.setInitialRoute(routeStr);
        //FlutterView設定佈局引數
        flutterView.setLayoutParams(matchParent);
        //將FlutterView設定進ContentView中,設定內容檢視
        this.setContentView(flutterView);
        return flutterView;
    }
    /**
     * 獲取引數資訊
     * 傳遞給flutterVIew
     */
    private void getIntentData(){
        String route = getIntent().getStringExtra("_route_");
        String params = getIntent().getStringExtra("_params_");
        JSONObject jsonObject = new JSONObject();

        try{
            jsonObject.put("pageParams",params);
        } catch (JSONException e){
            e.printStackTrace();
        }
        //字串是路由引數 + 業務引數
        //形式如下:test?{"pageParams":"{\"content\":\"這是測試內容\"}"}
        routeStr = route + "?" + jsonObject.toString();
        Log.d(TAG,"onCreate-route:" + route + "-params" + params);
        Log.d(TAG,"pnCreate-routeStr:" + routeStr);
    }

    /**
     * 外掛要實現的方法
     * @param methodCall
     * @param result
     */
    @Override
    public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
       switch(methodCall.method){
           case "showToast"://呼叫原生的toast
               String content = methodCall.argument("content");
               Toast.makeText(this, content, Toast.LENGTH_SHORT).show();
               break;
               default:
                   result.notImplemented();

       }
    }
}
複製程式碼

3.2 跳轉到FlutterActivity

在第一個fragment增加跳轉到這Activity程式碼:

    private void initListener(){
        btnNativeToFlutter.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //跳轉到FlutterMainActivity
                Map<String,Object> map = new HashMap<>();
                //並且攜帶業務引數
                map.put("content","這是測試內容");
                String jsonString = new Gson().toJson(map);
                String route = "test";

                Intent intent = new Intent(getActivity(), FlutterMainActivity.class);
                intent.putExtra("_route_",route);
                intent.putExtra("_params_",jsonString);
                startActivity(intent);

            }
        });
    }

複製程式碼

注意傳遞給FlutterMainActivity的路由引數是test,那麼需要在Flutter增加test頁面:

3.3.指定Flutter頁面

class Test extends StatefulWidget{

  final String content;//wowId
  Test({this.content});

  _TestState createState() => _TestState();

}

class _TestState extends State<Test>{

  @override
  void initState(){
    super.initState();
    print('content:' + widget.content);
  }

  @override
  Widget build(BuildContext context){
    return MaterialApp(
      home:new Scaffold(
        appBar: new AppBar(
          brightness: Brightness.light,
          title: new Text(
            'Flutter',
            style: new TextStyle(fontSize: 20,color:Color(0xFF1A1A1A)),

          ),
          centerTitle: true,
          elevation: 0,
          backgroundColor: Colors.blue,
          leading: new IconButton(
            icon:new Icon(Icons.arrow_back),
            color:Color(0xFF333333),
            onPressed: (){
              closeFlutter(context);
            },
          ),
        ),
        body: new Container(
          color: Colors.white,
          child: new ListView(
            children: <Widget>[
              new Padding(padding: EdgeInsets.only(top:100)),
              new Container(
                alignment: Alignment.center,
                child: new Text(
                  widget.content,
                  style: new TextStyle(
                    color: Colors.red,
                    fontSize: 40,
                    fontWeight: FontWeight.normal,
                    decoration: TextDecoration.none
                  ),
                ),
              ),

              new Container(
                width: 100,
                alignment: Alignment.center,
                child: new MaterialButton(
                  child: new Text("開啟原生的toast"),
                  color: Colors.greenAccent,
                  onPressed: (){
                    buttonClick();
                  }),
              ),
            ],
          ),
        ),
      ),
    );
  }
  //彈出toast
  void buttonClick(){
    ToastUtil.showToastInfo("哈哈哈");
  }

  //返回頁面
  void closeFlutter(BuildContext context){
    NavigatorUtil.close(context);
  }
}

複製程式碼

3.4.配置跳轉邏輯

Flutter專案的main.dart檔案配置如果路由引數是test的邏輯:

  //擷取跳轉到哪個頁面引數
  switch(pageName){
    case 'tab_fragment':
       return new TabFragment();
    case 'test'://test頁面
       //獲取業務引數
       String content = pageParams["content"] ?? "defaultContent";
       return new Test(content:content,);
  }
複製程式碼

3.5.效果

debug環境下效果如下:

debug環境下
可以看到debug下會有明顯的黑屏現象,那麼release會不會是這樣呢?

release環境下效果如下:

release環境下
可以看到release下原生跳轉到Flutter沒有了黑屏,且切換速度很快。

4.以普通Activity為載體

在第一種方式Fragment可以知道,Fragment通過在onCreateView方法裡建立FlutterView並返回即可與Flutter互動,那麼能不能在普通Activity通過setContentView方法把FlutterView設定顯示檢視,最終達到互動目的呢?下面嘗試一下:

/**
 * Android -> Flutter (普通Activity)
 *
 */
public class MyFlutterActivity extends AppCompatActivity implements MethodChannel.MethodCallHandler {
    private static final String TOAST_CHANNEL = "com.test.native_flutter/toast";
    private FlutterView flutterView;

    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceStae){
        super.onCreate(savedInstanceStae);

        String route = getIntent().getStringExtra("_route_");
        String params = getIntent().getStringExtra("_params_");
        JSONObject jsonObject = new JSONObject();
        try{
            jsonObject.put("pageParams",params);
        } catch(JSONException e){
            e.printStackTrace();

        }
        //建立FlutterView
        flutterView = Flutter.createView(this,getLifecycle(),route + "?" + jsonObject.toString());
        //設定顯示檢視
        setContentView(flutterView);
        //外掛註冊
        registerMethodChannel();
        
    }

    @Override
    public void onBackPressed(){
        if(flutterView != null){
            flutterView.popRoute();
        }else{
            super.onBackPressed();
        }
    }

    private void registerMethodChannel(){
        new MethodChannel(flutterView,TOAST_CHANNEL).setMethodCallHandler(this);
    }

    @Override
    public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
        switch(methodCall.method){
            case "showToast":
                //呼叫原生的Toast
                String content = methodCall.argument("content");
                Toast.makeText(this,content,Toast.LENGTH_SHORT).show();
                break;
            default:
                result.notImplemented();

        }
    }
}
複製程式碼

普通跳轉FlutterView
同樣也是可以的。

Flutter跳轉native方式就很簡單了,和彈出吐司一樣,在onMethodCall做跳轉就可以了,例如:

    // 自定義外掛
    String CHANNEL = "xxx.plugin";
    new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(new MethodCallHandler() {
      @Override
      public void onMethodCall(MethodCall call, Result result) {
        if (call.method.equals("routeName")) {
          Intent intent = new Intent(MainActivity.this, HomeActivity.class);
          MainActivity.this.startActivity(intent);
          result.success("success");
        } else {
          result.notImplemented();
        }
      }
    });
複製程式碼

可見Google團隊想的很全面。

五、總結

通過nativeFlutter兩個專案來達到混合開發的優勢是互不影響,在native不需要考慮Flutter是否影響本身,並通過閱讀FlutterActivityFlutterView部分原始碼,可以知道下面幾點:

  • 1.FlutterViewnativeFlutter的橋樑。
  • 2.開啟FlutterActivity,通過Intent傳入具體的路由值,再由FlutterView通過setInitialRoute方法設定Flutter中的window.defaultRouteName
  • 3.在FlutterActivityDelegate,實現對FlutterActivityFlutter頁面的宣告週期管理,FlutterView是繼承SurfaceView,而nativeFlutter之間的通訊是FlutterNativeView
  • 4.注意因為nativeFlutter是兩個專案,是可以分別單獨執行。

六、參考

Flutter原始碼分析之初始化篇一(android)

Flutter筆記--Flutter頁面嵌入Android Activity中

七、專案例子程式碼

github.com/KnightAndro…

相關文章