最近 Flutter 很火,相信長得帥的人都已經對它都有了初步的瞭解。
不過由於目前預設使用 Flutter 作為框架接管整個 App 進行開發,不夠靈活:一方面使用純 Flutter 開發需要在專案開始之前仔細評估是否有難以實現的功能;另一方面現有的 App 想使用 Flutter 的話很難全部轉換過去。
很容易想到在現有的 App 的基礎上加入 Flutter 作為部分畫面/功能的實現是一個理想的方案,也更有利於做技術嘗試和風險控制。
實際上目前 Flutter 官方提供了兩種方案用於給現有 App 加入 Flutter Module,另外還有一些第三方的方案,最近我做了一些嘗試,分享一些成果。
需要注意的是, 給現有 App 引入 Flutter Module 的功能還在實驗性的階段, APIs 和工具鏈處於未穩定階段,且需要切換到 master
分支(不穩定)使用。
Android
建立一個 Flutter module
假設在 some/path/MyApp
下是 Android 專案目錄
cd some/path
flutter create -t module --org com.example flutter_to_app
複製程式碼
會在 some/path/flutter_to_app
生成一個 Flutter Module
宿主 App 設定
需要在app/build.gradle
裡設定
android {
//...
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}
複製程式碼
讓 App 依賴 Flutter Module
有兩種方案,直接依賴原始碼和 aar 產物。
1. 依賴生成的 aar
cd ~/Documents/Android/flutter_to_app
flutter build aar
複製程式碼
// MyApp/app/build.gradle
android {
// ...
}
repositories {
maven {
//可以使用相對路徑或者絕對路徑
url 'some/path/flutter_to_app/build/host/outputs/repo'
}
}
dependencies {
// ...
releaseCompile ('com.example. flutter_to_app:flutter_release:1.0@aar') {
transitive = true
}
}
複製程式碼
可以用 flutter build aar --debug
生成 debug 依賴
// MyApp/app/build.gradle
dependencies {
// ...
debugCompile ('com.example.my_flutter:flutter_debug:1.0@aar') {
transitive = true
}
}
複製程式碼
2.直接依賴原始碼
依賴 aar 的方式有點麻煩,還需要到 Module 中編譯,所以也可以直接依賴原始碼編譯
在宿主 App settings.gradle
加入
// MyApp/settings.gradle
include ':app'
...
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
'flutter_to_app/.android/include_flutter.groovy'
))
複製程式碼
上面的File()
路徑是 flutter module 相對 host app 的路徑。binding 和 include_flutter.groovy
指令碼引入 flutter module 本身和相關的 plugin。
最後,依賴模組:
// MyApp/app/build.gradle
dependencies {
implementation project(':flutter')
}
複製程式碼
在 Android 專案中使用 Flutter Module
目前有兩種方式實現,分別在
io.flutter.facade.*
io.flutter.embedding.android.*
兩個包下, 第一種已經被 deprecated ,第二種還處於 technical preview 階段,所以兩種版本的 API 都還不穩定,但可以大概看一下兩種方式。
以前的方式(deprecated) ( io.flutter.facade )
通過使用 Flutter.createView
:
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
View flutterView = Flutter.createView(
MainActivity.this,
getLifecycle(),
"route1"
);
FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(600, 800);
layout.leftMargin = 100;
layout.topMargin = 200;
addContentView(flutterView, layout);
}
});
複製程式碼
通過使用 Flutter.createFragment
:
// MyApp/app/src/main/java/some/package/SomeActivity.java
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
FragmentTransaction tx = getSupportFragmentManager().beginTransaction();
tx.replace(R.id.someContainer, Flutter.createFragment("route1"));
tx.commit();
}
});
複製程式碼
建立View
和Fragment
都非常簡單,但是實際測試下來,啟動 View (FlutterFragment實際上也是通過 createView 來生成檢視的)會有啟動時間,體驗沒那麼無縫。
新的方式( io.flutter.embedding.android.* )
通過 FlutterView ( 繼承自 FrameLayout )
例項化 FlutterView 嵌入 Native
FlutterView flutterView = new FlutterView(this);
FrameLayout frameLayout = findViewById(R.id.framelayout);
frameLayout.addView(flutterView);
//建立一個 FlutterView 就可以了,這個時候還不會渲染。
//呼叫下面程式碼後才會渲染
flutterView.attachToFlutterEngine(flutterEngine);
複製程式碼
通過 FlutterFragment 開啟
通過 xml
<fragment
android:id="@+id/flutterfragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="io.flutter.embedding.android.FlutterFragment"
/>
複製程式碼
直接例項化
flutterFragment = new FlutterFragment.createDefault();
複製程式碼
通過 FlutterActivity 開啟
在 AndroidManifest.xml 中註冊
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:exported="true"
/>
複製程式碼
預設啟動方式
//預設路由為 '/'
Intent defaultFlutter = new FlutterActivity.createDefaultIntent(currentActivity);
startActivity(defaultFlutter);
複製程式碼
啟動到指定路由
Intent customFlutter = new FlutterActivity.IntentBuilder()
.initialRoute("someOtherRoute")
.build(currentActivity);
startActivity(customFlutter);
複製程式碼
FlutterEngine 快取機制
實際上,通過 API 和原始碼可以看出,新版的 Flutter 相關類io.flutter.embedding.android.*
完全重新設計了 Native 呼叫的方式,從包名(embedding)就可以看出是希望嵌入 Native, 其中一個重要的變化是加入了 FlutterEngine
的快取機制。
通過老的方式啟動 Flutter 的響應時間長包括了需要啟動FlutterEngine
的時間,可以理解為冷啟動,而且從原生的不同Activity / ViewController
啟動 Flutter 都需要啟動一個新的 FlutterEngine
,所以不僅第一次啟動 Flutter 時間長 ,每次啟動都會需要同樣的時間。比如下面的情況
Native A -> Flutter B -> Native C -> Flutter D
這樣從Native A
和 Native B
啟動時會例項化兩個FlutterEngine
。
這樣不僅慢,對資源的開銷也更多。
為了解決這個問題,新的解決方案引入了FlutterEngine
快取機制。
1. 使用 FlutterEngineCache
// 例項化 FlutterEngine.
FlutterEngine flutterEngine = new FlutterEngine(context);
// 預熱
flutterEngine
.getDartExecutor()
.executeDartEntrypoint(
DartEntrypoint.createDefault()
);
//放入 FlutterEngineCache
FlutterEngineCache
.getInstance()
.put("my_engine_id", flutterEngine);
//啟動 Activity 的時候使用
Intent intent = FlutterActivity
.withCachedEngine("my_engine_id")
.build();
startActivity(intent);
//例項化 Fragment
FlutterFragment flutterFragment = FlutterFragment
.withCachedEngine("my_engine_id")
.build();
複製程式碼
2. 繼承 FlutterFragment / FlutterActivity
自行處理儲存 FlutterEngine 的地方
public class MyFlutterFragment extends FlutterFragment {
@Override
@Nullable
protected FlutterEngine provideFlutterEngine(@NonNull Context context) {
//自行儲存 FlutterEngine 例項
return MyFlutterEngine.getFlutterEngine();
//比如 Application 中
return ((MyApp) context.getApplication).getFlutterEngine();
}
}
複製程式碼
public class MyFlutterActivity extends FlutterActivity {
@Nullable
@Override
public FlutterEngine provideFlutterEngine(@NonNull Context context) {
FlutterEngine flutterEngine;
//自行儲存 FlutterEngine 例項
flutterEngine = MyFlutterEngineCache.getFlutterEngine();
//比如 Application 中
flutterEngine = ((MyApp) getApplication()).getFlutterEngine();
return flutterEngine;
}
}
複製程式碼
3. 在 Activity 實現 FlutterEngineProvider 介面
public class MyActivity extends Activity implements FlutterEngineProvider {
@Override
@Nullable
FlutterEngine provideFlutterEngine(@NonNull Context context) {
//自行儲存 FlutterEngine 例項
return MyFlutterEngine.getFlutterEngine();
//比如 Application 中
return ((MyApp) context.getApplication).getFlutterEngine();
}
}
複製程式碼
FlutterBoost 方案
新一代 Flutter-Native 混合解決方案。 FlutterBoost是一個Flutter外掛,它可以輕鬆地為現有原生應用程式提供Flutter混合整合方案。FlutterBoost的理念是將Flutter像Webview那樣來使用。在現有應用程式中同時管理Native頁面和Flutter頁面並非易事。 FlutterBoost幫你處理頁面的對映和跳轉,你只需關心頁面的名字和引數即可(通常可以是URL)。
FlutterBoost 是閒魚開源處理 Flutter-Native 混合開發的解決方案,是一個熱門的方案,但和官方方案對比我認為有兩個重要的異同點:
- 當時閒魚設計這個庫其中的一個重要目的就是為了解決 FlutterEngine 無法重用的問題(當時 Flutter 團隊還沒有可以處理 FlutterEngine 重用的方案),而現在 Flutter 團隊推出的新的解決方案也可以解決這個問題。
- 目前 Flutter 官方的方案的細粒度更小,可以通過 View 的方式呼叫 Flutter ,也就是說你可以只將畫面中的某一個圖表用 Flutter 替換。
最後,官方的兩種方案一種已經被捨棄一種還處於實驗性階段,目前最新方案的Milestone
是12月,所以到時候再次評估可行性。而國內大廠基本上各自都有自己的解決方案,所以目前使用官方方案的話還需要仔細評估。