一、前言
Flutter
自從1.0版本釋出,現在越來越受歡迎,很多公司都在研究或者用在專案上。今天實踐一下Android
原生專案如何巢狀Flutter
頁面,具體原理就是Flutter
作為Android Module
出現在專案中,這樣就可以在已有的專案中使用,Android
專案也是一個工程,Flutter
專案也是一個工程,這樣就互不相關,也很好進行管理。廢話不多說,開始實踐。
二、目錄結構
首先講一下整個工程的結構:
1.建立Android工程
在建立Android工程前,新建一個資料夾(目錄),取名叫:mixProject,裡面在建立兩個資料夾分別是:flutter和native,示意圖如下:注意:後面flutter資料夾會刪除,這裡這是說明整個工程的目錄
下面就在native資料夾建立Android工程,File
->New
->New Project
:
2.建立Module模式的Flutter工程
建立工程之前先把flutte
資料夾目錄刪除,在mixProject
目錄下以Module形式建立Flutter工程,File
->New
->New Flutter Project
,這裡要注意,選型別的要選Flutter Module
,Flutter專案跟Android工程根資料夾是同級的,它不同於普通的Android module存在於Android工程根目錄下。
這樣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
))
複製程式碼
這樣要注意: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
專案的頁面入口。那麼下面看看這個類的原始碼,到底做了什麼?
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
實現上面三個介面主要是建立檢視,返回檢視以及監聽生命週期的回撥。下面回到FlutterActivity
,FLutterActivityDelegate
後面再分析:
//建立委託類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
物件就行,因為傳遞給委託類FlutterActivityDelegate
的ViewFactory
並沒有生成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
導航是通過MethodChannel
與Flutter
進行通訊,最終交由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.dart對defaultName更進一步的說明:
/// 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';
複製程式碼
註釋的意思如果沒有特定的路由,預設是**/和Android和IOS**如何設定該值方式和時機,再回到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);
}
}
複製程式碼
這是用於Flutter
和Native
之間交換資料的介面類,已知FlutterView
已經實現了SurfaceView
,而FlutterNativeView
負責FlutterView
和Flutter
之間的通訊,再使用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);
}
}
複製程式碼
可以看到最後通過FlutterView
的runFromBundle()
執行。
7.runFromBundle
public void runFromBundle(FlutterRunArguments args) {
this.assertAttached();
this.preRun();
this.mNativeView.runFromBundle(args);
this.postRun();
}
複製程式碼
呼叫FlutterNativeView
的runFromBundle
方法:
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值一樣,就能跳到對應的頁面。下面用一張圖簡單描述流程:
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
這行程式碼已經幫我們初始了FlutterMain
,FlutterNativeView
,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();
}
複製程式碼
看看效果圖:
可以看到只通過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();
}
複製程式碼
效果圖如下:
點選巢狀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
下會有明顯的黑屏現象,那麼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();
}
}
}
複製程式碼
同樣也是可以的。
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
團隊想的很全面。
五、總結
通過native
和Flutter
兩個專案來達到混合開發的優勢是互不影響,在native
不需要考慮Flutter
是否影響本身,並通過閱讀FlutterActivity
和FlutterView
部分原始碼,可以知道下面幾點:
- 1.
FlutterView
是native
和Flutter
的橋樑。 - 2.開啟
FlutterActivity
,通過Intent
傳入具體的路由值,再由FlutterView
通過setInitialRoute
方法設定Flutter
中的window.defaultRouteName
。 - 3.在
FlutterActivityDelegate
,實現對FlutterActivity
和Flutter
頁面的宣告週期管理,FlutterView
是繼承SurfaceView
,而native
和Flutter
之間的通訊是FlutterNativeView
。 - 4.注意因為
native
和Flutter
是兩個專案,是可以分別單獨執行。
六、參考
Flutter筆記--Flutter頁面嵌入Android Activity中