1、項目特點
項目是Flutter作為主工程,將Android module或SDK作為模塊嵌入到flutter中,與通常所熟悉的Android(或iOS)工程將flutter 為module嵌入到工程中有所不同。
2、業務需求
任意界面間的跳轉,不管是flutter頁面Android頁面亦或是SDK中的頁面,需要實現相互間的任意跳轉且按照順序返回(例如進入時A-B-C-D-C-D-B,返回時是B-D-C-D-C-B-A,不考慮啟動模式)
3、實現方式探索
我們都知道Android 要打開一個Flutter頁面(簡稱為A頁面吧),需要借助與FlutterEngine來開啟一個新的任務棧。
startActivity(FlutterActivity.withNewEngine().initialRoute("/second").build(ThreeActivity.this));
這樣可以開啟在Flutter代碼中注冊路由為“/second”的A頁面,但這個頁面加載速度會有些慢,因為每次都需要創建新的FlutterEngine,因此我們可以優化代碼為:
public class MyApplication extends FlutterApplication {@Overridepublic void onCreate() {super.onCreate();FlutterEngine flutterEngineInit = new FlutterEngine(this);//flutterEngineInit.// 開始執行Dart代碼以預熱FlutterEngineflutterEngineInit.getNavigationChannel().setInitialRoute("/second");flutterEngineInit.getDartExecutor().executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault());// 緩存FlutterActivity要使用的FlutterEngineFlutterEngineCache.getInstance().put("/second", flutterEngineInit);}
}
自定義Application并在AndroidManifest.xml中注冊,在自定義的application的onCreate()方法中將頁面提前綁定到創建好的FlutterEngine上,并放入到緩存中
<applicationandroid:label="untitled"android:name=".MyApplication"android:icon="@mipmap/ic_launcher"android:usesCleartextTraffic="true"></application>
這樣在打開A頁面時就可以,從緩存中獲取,加載體驗會明顯優于使用.withNewEngine()創建并打開A頁面
jumpTv.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {startActivity(FlutterActivity.withCachedEngine("/second").build(ThreeActivity.this));}});
注意:需要在AndroidManifest.xml中注冊FlutterActivity
<activityandroid:name="io.flutter.embedding.android.FlutterActivity"android:theme="@style/LaunchTheme"android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"android:hardwareAccelerated="true"android:windowSoftInputMode="adjustResize"/>
到這里你可以在Android中打開A頁面了。
4、遇到問題
但有一個很大的問題,如果A頁面需要與Android交互怎么辦?每次打開A頁面都是放在一個新的FlutterEngine中,FlutterEngine是相互隔離的,channel無法互通。而我們的項目作為一個flutter項目,APP啟動時,已經創建了FlutterEngine,且channel全部建立在這個engine中,上述方法打開的頁面是無法使用這些channel;并且flutter B頁面中也可以通過navigation.push打卡A頁面,那A頁面所在的FlutterEngine,即為B頁面所在的FlutterEngine。如此情況A頁面的在調用channel時會報如下錯誤:
2023-11-15 15:56:51.504 1528-10332/com.example.untitled E/flutter: [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: MissingPluginException(No implementation found for method jumpToAndroidWebviewPage on channel samples.flutter.jumpto.android)#0 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:165:7)<asynchronous suspension>
提示我在A頁面中調用的channel,沒有找到對應的實現。這是因為Android通過FlutterEngine開啟的A頁面,新的任務棧中沒有對應的實現。如何解決這個問題呢?
MethodChannel一般會是這樣寫:
public class MainActivity extends FlutterActivity implements MethodChannel.MethodCallHandler{@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);}@Overridepublic void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {super.configureFlutterEngine(flutterEngine);MethodChannel commonMethodChannel = new MethodChannel(flutterEngine.getDartExecutor(), "samples.flutter.jumpto.android");commonMethodChannel.setMethodCallHandler(this);}@Overridepublic void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {}
}
我注意到在創建channel的時候,接收兩個參數一個是messenger,一個是name。name好理解就是channel名稱,自己定義。而messenger,我們是通過flutterEngine.getDartExecutor()來獲取的一個BinaryMessenger的實現類對象。 也就是說我們只要能拿到每次打開A頁面的FlutterEngine對象,那就好解決這個問題了。
我們創建一個channel的管理類,方便我們在每個engine中實現channel(代碼有優化空間)
public class ChannelManage implements MethodChannel.MethodCallHandler, EventChannel.StreamHandler{private FlutterEngine flutterEngine;private Context context;EventChannel.EventSink jumpevents;public ChannelManage(FlutterEngine flutterEngine, Context context){this.flutterEngine = flutterEngine;this.context = context;MethodChannel commonMethodChannel = new MethodChannel(flutterEngine.getDartExecutor(), "samples.flutter.jumpto.android");commonMethodChannel.setMethodCallHandler(this);}@Overridepublic void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {if (call.method.equals("jumpToAndroidPage")) {Intent intent=new Intent(context, ContentActivity.class);context.startActivity(intent);result.success("跳轉");}else if(call.method.equals("jumpToAndroidSDKPage")){Intent intent=new Intent(context, JumpActivity.class);context.startActivity(intent);result.success("跳轉");}else if(call.method.equals("jumpToAndroidWebviewPage")){Intent intent=new Intent(context, ThreeActivity.class);context.startActivity(intent);result.success("跳轉");} else {result.notImplemented();}}@Overridepublic void onListen(Object arguments, EventChannel.EventSink events) {events.success("Android");jumpevents=events;}@Overridepublic void onCancel(Object arguments) {}
}
步驟3中的代碼改下:
我們在application中已經緩存了A頁面和綁定的FlutterEngine,從緩存那取出來,獲取對應的messenger設置給ChannelManage,這樣A頁面的Channel也可以使用啦。
jumpTv.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {new ChannelManage(FlutterEngineCache.getInstance().get("/second"),ThreeActivity.this);startActivity(FlutterActivity.withCachedEngine("/second").build(ThreeActivity.this));}});
但是不要高興太早,還記得我們要的需求不?任意界面間的跳轉,不管是flutter頁面Android頁面亦或是SDK中的頁面。
按照上面的方式假如有一個Android activityB 通過上述方法打開了flutter A頁面,A頁面又通過Channel新開了activityB頁面,activityB又打開了A頁面,(這里例子比較極端,現實中A頁面和activityB中間可能有其他頁面,但不妨礙會出現循環打開的場景)那會是什么樣子呢?
童年Andy 2023-11-11 16.19.28
我發現在返回到A頁面倒數第二次打開的地方,屏幕上沒有任何界面,且不能響應系統的手勢放回。這肯定是不行啊,沒有按原路返回啊,更大的問題是APP不能用了啊。
這是怎么回事呢?
其實是因為用了FlutterEngineCache,我們每次取到的都是一個FlutterEngine,那么當多次使用時,FlutterEngine就被放在了最后一次出現的位置,原位置將是一個空的engine,那這怎么玩。
再回過頭來看下,如果每次都新建engine開啟A頁面呢?
回顧下上面的代碼(中間寫的廢話太多了)
startActivity(FlutterActivity.withNewEngine().initialRoute("/second").build(ThreeActivity.this));
這肯定沒問題了吧,試下。靠,Channel又報錯了.......。
那就想法搞到每次的FlutterEngine不就行了嗎?
看API....,哎呀,這玩意沒有get?FlutterEngine的API啊。這怎么玩。
但是如果我寫一個MFlutterActivity?extends?FlutterActivity,然后重寫configureFlutterEngine方法不就可以了嗎?(MainActivity不就是這樣搞的嗎)
startActivity(MFlutterActivity.withNewEngine().initialRoute("/second").build(ThreeActivity.this));
public class MFlutterActivity extends FlutterActivity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);}@Overridepublic void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {new ChannelManage(flutterEngine,this);}}
注意:需要在AndroidManifest.xml中注冊MFlutterActivity
來一把試試,結果不行。這里的configureFlutterEngine不會回調,不應該啊,MainActivity為什么就可以呢?
想不通啊想不通。翻源代碼。
發現如下注釋(走了很多彎路,不贅述了,也嘗試過反射獲取engine對象,我沒有成功,小伙伴們可以試試)
/*** Constructor that allows this {@code NewEngineIntentBuilder} to be used by subclasses of* {@code FlutterActivity}.** <p>Subclasses of {@code FlutterActivity} should provide their own static version of {@link* #withNewEngine()}, which returns an instance of {@code NewEngineIntentBuilder} constructed* with a {@code Class} reference to the {@code FlutterActivity} subclass, e.g.:** <p>{@code return new NewEngineIntentBuilder(MyFlutterActivity.class); }*/public NewEngineIntentBuilder(@NonNull Class<? extends FlutterActivity> activityClass) {this.activityClass = activityClass;}
啥意思?這意思是不是要自己重寫withNewEngine方法?干吧,直接把系統的代碼粘貼復制一下試試。
/*** Creates an {@link NewEngineIntentBuilder}, which can be used to configure an {@link Intent} to* launch a {@code FlutterActivity} that internally creates a new {@link* io.flutter.embedding.engine.FlutterEngine} using the desired Dart entrypoint, initial route,* etc.** @return The engine intent builder.*/@NonNullpublic static NewEngineIntentBuilder withNewEngine() {return new NewEngineIntentBuilder(FlutterActivity.class);}
APP起來,起來吧!!!
童年Andy 2023-11-11 14.30.39
5、總結:
實現flutter和Activity間的任意跳轉一定要實現對Channel的統一管理和自定義MFlutterActivity繼承FlutterActivity,用MFlutterActivity去執行加載flutter頁面的代碼開啟新的engine,且一定要重寫withNewEngine方法,否則Activity的生命周期以及configureFlutterEngine是不會執行的,也就沒辦法拿到engine,以便為統一管理的Channel設置messenger。
6、原代碼
最后上demo代碼,自取。
flutter_android_jump: 實現flutter工程下嵌入SDK、Android module后,flutter與Android界面間的任意跳轉和通信