Flutter 說到底隻是一個 UI 架構,很多功能都需要通過原生的 Api 來實作,那麼就會涉及到 Flutter 和 Native 的互動,因為本人不懂 iOS 開發,是以隻能講下 Flutter 同 Android 的互動。
Android 項目配置 Flutter 依賴
既然是互互相動,那麼需要準備一個 Android 項目。接着就需要建立 flutter module,讓 Android 項目依賴,建立的方法可以參考官網
Flutter Wiki,雖然是官網提供的方法,但是完全按照這個步驟來,還是會有坑的,這邊就慢慢一步步解決坑。
如果你用的是 Android Studio 進行開發的話,直接打開底部的 Terminal,直接建立 flutter module 依賴
flutter create -t module flutter_native_contact
至于 module 名可以随意填寫,module 建立完後結構大概是這樣的
接着切換到 module 下的 .android 檔案夾,接着有坑來了,官網提供的方法是
./gradlew flutter:assembleDebug
可能會提示指令不存在,那麼直接通過
gradlew flutter:assembleDebug
來運作,等它自動跑完後,打開根目錄下的
settings.gradle
檔案,加入官網提供的 gradle 代碼
setBinding(new Binding([gradle: this])) // new
evaluate(new File( // new
settingsDir.parentFile, // new
'flutter_native_contact/.android/include_flutter.groovy' // new
)) // new
你以為這裡沒坑,真是圖樣圖森破,沒坑是不可能的,編譯器大爺可能會給你甩這麼個錯誤
很明顯可以看出是找不到我們的檔案,是以把檔案名路徑給補全
evaluate(new File( // new
settingsDir.parentFile, // new
'FlutterNativeContactDemo/flutter_native_contact/.android/include_flutter.groovy' // 這裡補全路徑
))
接着打開原有項目下,原有項目下,原有項目下的 app 中的
build.gradle
檔案,在 android 下加上如下代碼
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
這個必須要加,不要問為什麼,我也不知道為什麼,最後在項目下添加 flutter module 的依賴就完成了。這個過程告訴我們一個什麼道理呢?*不要以為官網的都對,官網講的也不是完全可信的,時不時給你來個坑就能卡你老半天。
原生界面加載 Flutter 頁面
那麼如何在原生界面顯示 Flutter 界面呢,這個就需要通過 FlutterView 來實作了,Flutter 這個類提供了
createView
和
createFragment
兩個方法,分别用于傳回 FlutterView 和 FlutterFragment 執行個體,FlutterFragment 的實作原理也是通過 FlutterView 來實作的,可以簡單看下 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 FlutterView onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// 最後還是挺過 createView 方法來生成頁面,隻不過直接放在 fragment,
// 放在 fragment 會比直接 使用 FlutterView 更友善管理,例如實作 ViewPager 等
return Flutter.createView(getActivity(), getLifecycle(), mRoute);
}
}
createFragment 方式加載
在原生頁面顯示 Flutter 界面的第一種方式就是加載 FlutterFragment,看個比較簡單的例子吧
<?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"
tools:context=".MainActivity">
<!-- 這個布局用于加載 fragment -->
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/flutter_fragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_marginBottom="50dp"
android:src="@drawable/ic_add_white_36dp"
app:fabSize="auto"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</android.support.constraint.ConstraintLayout>
在 Activity 可以直接通過傳回 FlutterFragment 加載到 FrameLayout 即可
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, Flutter.createFragment("route_flutter"))
.commit()
}
}
這樣就把 Flutter 頁面加載到原生界面了,會通過傳遞的路由值在 dart 層進行查找,是以接着就需要編寫 Flutter 界面
/// runApp 内部值也可以直接傳入 _buildWidgetForNativeRoute 方法
/// 這邊在外層嵌套一層 MaterialApp 主要是防止一些不必要的麻煩,
/// 例如 MediaQuery 這方面的使用等
void main() => runApp(FlutterApp());
class FlutterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: _buildWidgetForNativeRoute(window.defaultRouteName),
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Color(0XFF008577),
accentColor: Color(0xFFD81B60),
primaryColorDark: Color(0xFF00574B),
iconTheme: IconThemeData(color: Color(0xFFD81B60)),
),
);
}
}
/// 該方法用于判斷原生界面傳遞過來的路由值,加載不同的頁面
Widget _buildWidgetForNativeRoute(String route) {
switch (route) {
case 'route_flutter':
return GreetFlutterPage();
// 預設的路由值為 '/',是以在 default 情況也需要傳回頁面,否則 dart 會報錯,這裡預設傳回空頁面
default:
return Scaffold();
}
}
class GreetFlutterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('NativeMessageContactPage'),
),
body: Center(
child: Text(
'This is a flutter fragment page',
style: TextStyle(fontSize: 20.0, color: Colors.black),
),
),
);
}
}
運作後可以看到頁面加載出來了,不過會有一段時間的空白,這個在正式打包後就不會出現,是以不必擔心。最後的頁面應該是這樣的
createView 方式加載
接着看下 createView 方法,說白了,第一種方法最後還是會通過該方式實作
@NonNull
public static FlutterView createView(@NonNull final Activity activity, @NonNull final Lifecycle lifecycle, final String initialRoute) {
// 互動前的一些初始化工作,需要完成才可以繼續下一步,同時需要保證目前線程為主線程
// Looper.myLooper() == Looper.getMainLooper(),否則會甩你一臉的 IllegalStateException
FlutterMain.startInitialization(activity.getApplicationContext());
FlutterMain.ensureInitializationComplete(activity.getApplicationContext(), null);
final FlutterNativeView nativeView = new FlutterNativeView(activity);
// 将 flutter 頁面綁定到相應的 activity
final FlutterView flutterView = new FlutterView(activity, null, nativeView) {
// ......
};
// 将路由值傳到 flutter 層,并加載相應的頁面,
if (initialRoute != null) {
flutterView.setInitialRoute(initialRoute);
}
// 綁定 lifecycle,友善生命周期管理,同 activity 綁定
// 不熟悉 LifeCycle 的同學可以自行網上查找資料
lifecycle.addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
public void onCreate() {
// 配置一些參數,傳遞到 flutter 層
final FlutterRunArguments arguments = new FlutterRunArguments();
arguments.bundlePath = FlutterMain.findAppBundlePath(activity.getApplicationContext());
arguments.entrypoint = "main";
// 最終會調用方法 nativeRunBundleAndSnapshotFromLibrary,這是一個 native 方法,進行互動
flutterView.runFromBundle(arguments);
// 進行注冊
GeneratedPluginRegistrant.registerWith(flutterView.getPluginRegistry());
}
// ......
});
return flutterView;
}
通過 createView 方法傳回的 FlutterView,通過設定 Layoutparams 參數就可以添加到相應的布局上,還有一種直接通過 addContentView 方式進行加載,這裡直接修改原有代碼,
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_main) 不需要這一步了
val flutterView = Flutter.createView(this@ContactActivity, lifecycle, "route_flutter")
val lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
addContentView(flutterView, lp) // 直接加載到 activity 頁面
}
但是通過這樣加載的話,那麼整個頁面都是 flutter 的頁面。那麼之前的效果的 FAB 則不會被加載出來了,即使沒有省略
setContentView(R.layout.activity_main)
方法,這個頁面的 xml 布局也會被覆寫。
PlantformChannel
那麼能夠在原生界面顯示 flutter 頁面了,如何互互相動呢,這就需要通過 PlantformChannel 來執行了,PlantformChannel 主要有三種類型,BasicMessageChannel,MethodChannel,EventChannel。通過檢視源碼可以發現,三個 Channel 的實作機制類似,都是通過 BinaryMessenger 進行資訊交流,每個 Channel 通過傳入的 channel name 進行區分,是以在注冊 Channel 的時候必須要保證 channel name 是唯一的,同時需要傳入一個 BinaryMessageHandler 執行個體,用于傳遞資訊的處理,當 Handler 處理完資訊後,會傳回一個 result,然後通過 BinaryMessenger 将 result 傳回到 Flutter 層。如果需要深入了解這邊推薦一篇文章
深入了解Flutter PlatformChannel接下來直接看例子吧,在建立 PlatformChannel 的時候需要傳入一個 BinaryMessenger 執行個體,通過檢視 FlutterView 的源碼可以發現,FlutterView 就是一個 BinaryMessenger 在 Android 端的實作,是以呢,可以直接通過前面介紹的
Flutter.createView
方法擷取注冊 Channel 時的 BinaryMessenger 執行個體了,真是得來全部費工夫~因為通信的方法可能在多個界面會使用,是以還是封裝一個通用類來處理會比較合理
BasicMessageChannel
BasicMessageChannel 用于傳遞字元串和半結構化的資訊。
class FlutterPlugin(private val flutterView: FlutterView) :BasicMessageChannel.MessageHandler<Any>{
companion object {
private const val TAG = "FlutterPlugin"
@JvmStatic
fun registerPlugin(flutterView: FlutterView): FlutterPlugin {
// channel name 需要保持兩側一緻
val messageChannel =
BasicMessageChannel(flutterView, Constant.MESSAGE_CHANNEL_NAME, StandardMessageCodec.INSTANCE) // MessageCodec 有多種實作方式,可以參考推薦的文章
val instance = FlutterPlugin(flutterView)
messageChannel.setMessageHandler(instance) // 注冊處理的 Hnadler
return instance
}
}
override fun onMessage(`object`: Any?, reply: BasicMessageChannel.Reply<Any>?) {
// 簡單的将從 Flutter 傳過來的消息進行吐司,同時傳回自己的互動資訊
// `object` 中包含的就是 Flutter 層傳遞過來的資訊,reply 執行個體用于傳遞資訊到 Flutter 層
Toast.makeText(flutterView.context, `object`.toString(), Toast.LENGTH_LONG).show()
reply?.reply("\"Hello Flutter\"--- an message from Android")
}
}
接着就需要有個 FlutterView 用來注冊,建立一個 Activity,用于加載 Flutter 頁面
class ContactActivity : AppCompatActivity() {
private lateinit var plugin: FlutterPlugin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 傳入路由值,需要在 flutter 層生成相應的界面
val flutterView = Flutter.createView(this@ContactActivity, lifecycle, "route_contact")
val lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
addContentView(flutterView, lp)
plugin = FlutterPlugin.registerPlugin(flutterView)
}
override fun onDestroy() {
super.onDestroy()
}
}
那麼我們就要在 Flutter 界面的
_buildWidgetForNativeRoute
方法加入新路由值對應的界面
Widget _buildWidgetForNativeRoute(String route) {
switch (route) {
// ...
case 'route_contact':
return FlutterContactPage();
default:
return Scaffold();
}
}
class FlutterContactPage extends StatelessWidget {
// 注冊對應的 channel,要保證 channel name 和原生層是一緻的
final BasicMessageChannel _messageChannel =
BasicMessageChannel(MESSAGE_CHANNEL_NAME, StandardMessageCodec());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Page'),
),
// 簡單放一個按鈕,通過 channel 傳輸消息過去,同時将原生層傳回的消息列印出來
body: RaisedButton(
onPressed: () {
_messageChannel
.send('"Hello Native" --- an message from flutter')
.then((str) {
print('Receive message: $str');
});
},
child: Text('Send Message to Native'),
),
);
}
}
最後的效果小夥伴可以自行執行,點選按鈕後會彈出吐司,吐司内容就是 Flutter 傳遞的資訊,同時在控制台可以看到從原生層傳回的資訊。
MethodChannel
MethodChannel 用于傳遞方法調用(method invocation)
直接在上述例子中進行修改,例如在 Flutter 頁面中實作 Activity 的 finish 方法,并傳遞參數到前一個界面,先做 Flutter 頁面的修改,在 AppBar 上增加一個傳回按鈕,用于傳回上層頁面
class FlutterContactPage extends StatelessWidget {
// 注冊對應的 channel,要保證 channel name 和原生層是一緻的
final BasicMessageChannel _messageChannel =
BasicMessageChannel(MESSAGE_CHANNEL_NAME, StandardMessageCodec());
final MethodChannel _methodChannel = MethodChannel(METHOD_CHANNEL_NAME);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: InkWell(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Icon(Icons.arrow_back),
),
onTap: () {
_methodChannel
// invokeMethod 第一個值用于傳遞方法名,第二個值用于傳遞參數,
// 這邊簡單的傳遞一個字元串,當然也可以傳遞别的類型,map,list 等等
.invokeMethod<bool>('finishActivity', 'Finish Activity')
.then((result) { // 這邊會傳回一個結果值,通過判斷是否成功來列印不同的資訊
print('${result ? 'has finish' : 'not finish'}');
});
},
),
title: Text('Flutter Page'),
),
body: // ...
);
}
}
同時,我們需要在 FlutterPlugin 這個類中,做些必要的修改,首先需要實作
MethodCallHandler
接口,該接口中需要實作
onMethodCall
方法,通過擷取調用的方法名和參數值,進行相應的處理
class FlutterPlugin(private val flutterView: FlutterView) :
MethodChannel.MethodCallHandler, BasicMessageChannel.MessageHandler<Any> {
companion object {
private const val TAG = "FlutterPlugin"
@JvmStatic
fun registerPlugin(flutterView: FlutterView): FlutterPlugin {
val instance = FlutterPlugin(flutterView)
val methodChannel = MethodChannel(flutterView, Constant.METHOD_CHANNEL_NAME)
// ...
messageChannel.setMessageHandler(instance)
return instance
}
}
// ....
// call 中攜帶了 Flutter 層傳遞過來的方法名和參數資訊
// 可以分别通過 call.method 和 call.arguments 來擷取
override fun onMethodCall(call: MethodCall?, result: MethodChannel.Result?) {
when (call?.method) {
"finishActivity" -> {
val activity = flutterView.context as Activity
val info = call.arguments.toString()
val intent = Intent().apply {
putExtra("info", info)
}
activity.setResult(Activity.RESULT_OK, intent)
activity.finish()
// 成功時候通過 result.success 傳回值,
// 如果發生異常,通過 result.error 傳回異常資訊
// Flutter 通過 invokeMethod().then() 來處理正常結束的邏輯
// 通過 catchError 來處理發生異常的邏輯
result?.success(true)
}
// 如果未找到對應的方法名,則通過 result.notImplemented 來傳回異常
else -> result?.notImplemented()
}
}
最終的效果,當點選傳回按鈕的時候,會将 Flutter 層通過 invokeMethod 傳遞的 arguments 屬性吐司出來,同時,控制台會列印出 "has finish" 的資訊
EventChannel
EventChannel 用于資料流(event streams)的通信
EventChannel 的實作方式也類似,EventChannel 可以持續傳回多個資訊到 Flutter 層,在 Flutter 層的表現就是一個 stream,原生層通過 sink 不斷的添加資料,Flutter 層接收到資料的變化就會作出新相應的處理。在 Android 端實作狀态的監聽可以通過廣播來實作。直接看例子,還是修改上述代碼
class FlutterPlugin(private val flutterView: FlutterView) :
MethodChannel.MethodCallHandler, EventChannel.StreamHandler, BasicMessageChannel.MessageHandler<Any> {
private var mStateChangeReceiver: BroadcastReceiver? = null
companion object {
private const val TAG = "FlutterPlugin"
const val STATE_CHANGE_ACTION = "com.demo.plugins.action.StateChangeAction"
const val STATE_VALUE = "com.demo.plugins.value.StateValue"
@JvmStatic
fun registerPlugin(flutterView: FlutterView): FlutterPlugin {
// ...
val streamChannel = EventChannel(flutterView, Constant.STREAM_CHANNEL_NAME)
val instance = FlutterPlugin(flutterView)
methodChannel.setMethodCallHandler(instance)
streamChannel.setStreamHandler(instance)
messageChannel.setMessageHandler(instance)
return instance
}
}
// 實作 StreamHandler 需要重寫 onListen 和 onCancel 方法
// onListen 不會每次資料改變就會調用,隻在 Flutter 層,eventChannel 訂閱廣播
// 的時候調用,當取消訂閱的時候則會調用 onCancel,
// 是以當開始訂閱資料的時候,注冊接收資料變化的關閉,
// 在取消訂閱的時候,将注冊的廣播登出,防止記憶體洩漏
override fun onListen(argument: Any?, sink: EventChannel.EventSink?) {
mStateChangeReceiver = createEventListener(sink)
flutterView.context.registerReceiver(mStateChangeReceiver, IntentFilter(STATE_CHANGE_ACTION))
}
override fun onCancel(argument: Any?) {
unregisterListener()
}
// 在 activity 被銷毀的時候,FlutterView 不一定會調用銷毀生命周期,或者會延時調用
// 這就需要手動去登出一開始注冊的廣播了
fun unregisterListener() {
if (mStateChangeReceiver != null) {
flutterView.context.unregisterReceiver(mStateChangeReceiver)
mStateChangeReceiver = null
}
}
private fun createEventListener(sink: EventChannel.EventSink?):
BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (TextUtils.equals(intent?.action, STATE_CHANGE_ACTION)) {
// 這邊廣播隻做簡單的接收一個整數,然後通過 sink 傳遞到 Flutter 層
// 當然,sink 還有 error 方法,用于傳遞發生的錯誤資訊,
// 以及 endOfStream 方法,用于結束接收
// 在 Flutter 層分别有 onData 對應 success 方法,onError 對應 error 方法
// onDone 對應 endOfStream 方法,根據不同的回調處理不同的邏輯
sink?.success(intent?.getIntExtra(STATE_VALUE, -1))
}
}
}
}
在 Flutter 層,通過對 stream 的監聽,對傳回的資料進行處理,為了展現出變化,這邊修改成 SatefulWidget 來存儲狀态
class FlutterContactPage extends StatefulWidget {
@override
_FlutterContactPageState createState() => _FlutterContactPageState();
}
class _FlutterContactPageState extends State<FlutterContactPage> {
final MethodChannel _methodChannel = MethodChannel(METHOD_CHANNEL_NAME);
final EventChannel _eventChannel = EventChannel(STREAM_CHANNEL_NAME);
final BasicMessageChannel _messageChannel =
BasicMessageChannel(MESSAGE_CHANNEL_NAME, StandardMessageCodec());
StreamSubscription _subscription;
var _receiverMessage = 'Start receive state'; // 初始的狀态值
@override
void initState() {
super.initState();
// 當頁面生成的時候就開始監聽資料的變化
_subscription = _eventChannel.receiveBroadcastStream().listen((data) {
setState(() {
_receiverMessage = 'receive state value: $data'; // 資料變化了,則修改資料
});
}, onError: (e) {
_receiverMessage = 'process error: $e'; // 發生錯誤則顯示錯誤資訊
}, onDone: () {
_receiverMessage = 'receive data done'; // 發送完畢則直接顯示完畢
}, cancelOnError: true);
}
@override
void dispose() {
super.dispose();
_subscription.cancel(); // 當頁面銷毀的時候需要将訂閱取消,防止記憶體洩漏
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: InkWell(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Icon(Icons.arrow_back),
),
onTap: () {
// MethodChannel demo
_methodChannel
.invokeMethod<bool>('finishActivity', _receiverMessage)
.then((result) {
print('${result ? 'has finish' : 'not finish'}');
}).catchError((e) {
print('error happend: $e');
});
},
),
title: Text('Flutter Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
// EventChannel demo,頁面直接顯示資訊的變化
child: Text(
_receiverMessage,
style: TextStyle(fontSize: 20.0, color: Colors.black),
),
),
// BasicMessageChannel demo
RaisedButton(
onPressed: () {
_messageChannel
.send('"Hello Native" --- an message from flutter')
.then((str) {
print('Receive message: $str');
});
},
child: Text('Send Message to Native'),
),
],
),
),
);
}
}
同時,需要在 Activity 層調用一個定時任務不斷的發送廣播
class ContactActivity : AppCompatActivity() {
private var timer: Timer? = null
private var task: TimerTask? = null
private lateinit var random: Random
private lateinit var plugin: FlutterPlugin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
random = Random() // 生成随機整數
val flutterView = Flutter.createView(this@ContactActivity, lifecycle, "route_contact")
val lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
addContentView(flutterView, lp)
plugin = FlutterPlugin.registerPlugin(flutterView)
timer = Timer() // 定時器
task = timerTask { // 定時任務
sendBroadcast(Intent(FlutterPlugin.STATE_CHANGE_ACTION).apply {
putExtra(FlutterPlugin.STATE_VALUE, random.nextInt(1000))
})
}
timer?.schedule(task, 3000, 2000) // 延時 3s 開啟定時器,并 2s 發送一次廣播
}
override fun onDestroy() {
super.onDestroy()
// 頁面銷毀的時候需要将定時器,定時任務銷毀
// 同時登出 Plugin 中注冊的廣播,防止記憶體洩漏
timer?.cancel()
timer = null
task?.cancel()
task = null
plugin.unregisterListener()
}
}
最後的實作效果大概是這樣的
Flutter 同 Android 端的互動到這講的差不多了,和 iOS 的互動其實也類似,隻不過在 Android 端通過 FlutterNativeView 來作為 Binarymessenger 的實作,在 iOS 端通過 FlutterBinaryMessenger 協定實作,原理是一緻的。至于 Flutter 插件,其實作也是通過以上三種互動方式來實作的,可能我們目前通過 FlutterView 來作為 BinaryMessenger 執行個體,插件會通過 PluginRegistry.Registrar 執行個體的 messenger() 方法來擷取 BinaryMessenger 執行個體。
需要了解插件的寫法也可以直接檢視官方提供的檢測電量插件:
Flutter Battery Plugin在 Flutter 上顯示原生的控件
在日常開發過程中,可能會遇到這麼一種情況,Flutter 中沒有控件,但是在原生有,比如地圖控件,那麼就需要在 Flutter 顯示原生的控件了,那麼就需要用到
AndroidView
UiKitView
來加載原生的控件,這邊以
GoogleMapPlugin
為例
class _GoogleMapState extends State<GoogleMap> {
// 省略部分代碼
@override
Widget build(BuildContext context) {
// 省略部分代碼
// 判斷目前裝置是否 android 裝置,或者 iOS 裝置
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'plugins.flutter.io/google_maps', // viewType 需要同原生端對應,來加載對應的 view
onPlatformViewCreated: onPlatformViewCreated,
// ....
);
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
return UiKitView(
viewType: 'plugins.flutter.io/google_maps',
onPlatformViewCreated: onPlatformViewCreated,
// ....
);
}
return Text(
'$defaultTargetPlatform is not yet supported by the maps plugin');
}
這邊隻貼出關鍵部分的代碼,多餘的代碼省略,完整代碼可以通過上述連結檢視
接着看下
Android
端的代碼
public class GoogleMapsPlugin implements Application.ActivityLifecycleCallbacks {
// 省略部分代碼
public static void registerWith(Registrar registrar) {
if (registrar.activity() == null) {
// When a background flutter view tries to register the plugin, the registrar has no activity.
// We stop the registration process as this plugin is foreground only.
return;
}
final GoogleMapsPlugin plugin = new GoogleMapsPlugin(registrar);
registrar.activity().getApplication().registerActivityLifecycleCallbacks(plugin);
// 通過 registerViewFactory 方法注冊相應的 PlatformViewFactory,
// 其中第一個參數就是 Flutter 端對應的 viewType 參數值
registrar.platformViewRegistry()
.registerViewFactory(
"plugins.flutter.io/google_maps", new GoogleMapFactory(plugin.state, registrar));
}
// 省略部分代碼
}
那麼所有的顯示工作都放到
GoogleMapFactory
這個類中了
public class GoogleMapFactory extends PlatformViewFactory {
// 省略部分代碼
@SuppressWarnings("unchecked")
@Override
public PlatformView create(Context context, int id, Object args) {
Map<String, Object> params = (Map<String, Object>) args;
final GoogleMapBuilder builder = new GoogleMapBuilder();
// 省略屬性設定代碼
// 通過 `GoogleMapBuilder` 設定一些初始屬性
return builder.build(id, context, mActivityState, mPluginRegistrar);
}
}
GoogleMapFactory
繼承
PlatformViewFactory
并重寫
create
方法,傳回一個
PlatformView
執行個體,這個執行個體通過
GoogleMapBuilder
進行初始化
// GoogleMapOptionsSink -> Receiver of GoogleMap configuration options.
class GoogleMapBuilder implements GoogleMapOptionsSink {
// 省略部分代碼
GoogleMapController build(
int id, Context context, AtomicInteger state, PluginRegistry.Registrar registrar) {
final GoogleMapController controller =
new GoogleMapController(id, context, state, registrar, options);
controller.init();
controller.setMyLocationEnabled(myLocationEnabled);
controller.setMyLocationButtonEnabled(myLocationButtonEnabled);
controller.setIndoorEnabled(indoorEnabled);
controller.setTrafficEnabled(trafficEnabled);
controller.setTrackCameraPosition(trackCameraPosition);
controller.setInitialMarkers(initialMarkers);
controller.setInitialPolygons(initialPolygons);
controller.setInitialPolylines(initialPolylines);
controller.setInitialCircles(initialCircles);
controller.setPadding(padding.top, padding.left, padding.bottom, padding.right);
return controller;
}
// 省略部分 set 方法代碼
}
GoogleMapBuilder
實作了
GoogleMapOptionsSink
這個接口,主要用于接收一些地圖屬性參數,通過
build
方法最終傳回的是一個
GoogleMapController
執行個體
final class GoogleMapController
implements Application.ActivityLifecycleCallbacks,
// 這裡省略了一些地圖處理的相關接口
MethodChannel.MethodCallHandler,
PlatformView {
GoogleMapController(
int id,
Context context,
AtomicInteger activityState,
PluginRegistry.Registrar registrar,
GoogleMapOptions options) {
// 省略參數 set 代碼
methodChannel =
new MethodChannel(registrar.messenger(), "plugins.flutter.io/google_maps_" + id);
methodChannel.setMethodCallHandler(this);
}
@Override
public View getView() {
return mapView;
}
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
// 省略實作代碼,switch .. case
}
@Override
public void dispose() {
if (disposed) {
return;
}
disposed = true;
methodChannel.setMethodCallHandler(null);
mapView.onDestroy();
registrar.activity().getApplication().unregisterActivityLifecycleCallbacks(this);
}
}
GoogleMapController
這個類實作的接口比較多,這裡主要看兩個接口
-
對應實作的方法為MethodChannel.MethodCallHandler
方法,這裡就是用于處理onMethodCall
層調用原生的方法了,和前面介紹互動的一緻Flutter
-
PlatformView
getView
方法,dispose
傳回一個getView
即為需要在View
層顯示的控件了,Flutter
方法用于處理一些生命周期相關的邏輯,銷毀會造成記憶體洩漏的執行個體dispose
同時在初始化該類的時候,注冊了相應的
MethodChannel
,用于兩端的互動,那麼在
Flutter
端是哪裡注冊的
channel
呢,答案是
controller
檔案下的
GoogleMapController
類
class GoogleMapController {
GoogleMapController._(
this.channel,
CameraPosition initialCameraPosition,
this._googleMapState,
) : assert(channel != null) {
channel.setMethodCallHandler(_handleMethodCall);
}
static Future<GoogleMapController> init(
int id,
CameraPosition initialCameraPosition,
_GoogleMapState googleMapState,
) async {
assert(id != null);
final MethodChannel channel =
MethodChannel('plugins.flutter.io/google_maps_$id');
await channel.invokeMethod<void>('map#waitForMap');
return GoogleMapController._(
channel,
initialCameraPosition,
googleMapState,
);
}
@visibleForTesting
final MethodChannel channel;
// 省略無關代碼
}
當使用的時候,
GoogleMap
隻負責顯示視圖,屬性操作通過
GoogleMapController
來進行設定,完美的分擔相應的職責
iOS
端的
UiKitView
處理過程也類似,在使用過程中,需要注意
- 嵌入原生
是一個昂貴的操作,是以應當避免在view
能夠實作的情況下去使用它flutter
-
的繪制和其他任何view
一樣,flutter widget
的轉換也同樣使用view
- 元件會撐滿所有可獲得控件,是以它的父元件需要提供一個布局邊界
-
需要 api 版本 20 及以上AndroidView
仿照 GoogleMap 撸一個
寫個練手的小 demo,在
Flutter
層顯示
Android
的
TextView
,至于功能,就做一個設定
Text
内容和文字大小
實作 Flutter 端的代碼
const _textType = "com.demo.plugin/textview"; // 用于注冊 AndroidView
const _textMethodChannel = "com.demo.plugin/textview_"; // 用于注冊 MethodChannel
// 參考 GoogleMap,通過 controller 來實作方法的互動,view 隻負責展示
class TextController {
final MethodChannel _channel;
// 在構造函數注冊 MethodChannel
TextController(int _id) : _channel = MethodChannel('$_textMethodChannel$_id');
// 設定文字方法
Future<void> setText(String text) {
assert(text != null);
return _channel.invokeMethod("text#setText", text);
}
// 設定文字大小方法
Future<void> setTextSize(double size) {
assert(size != null);
return _channel.invokeMethod("text#setTextSize", size);
}
}
// 用于給展示的 view 設定 controller
typedef void TextViewCreateWatcher(TextController controller);
// 隻用于展示
class TextView extends StatefulWidget {
final TextViewCreateWatcher watcher;
TextView(this.watcher, {Key key}) : super(key: key);
@override
_TextViewState createState() => _TextViewState();
}
class _TextViewState extends State<TextView> {
@override
Widget build(BuildContext context) {
// 目前隻做 AndroidView, UiKitView 有興趣可自行搞定
return defaultTargetPlatform == TargetPlatform.android
? AndroidView(
viewType: _textType,
onPlatformViewCreated: _onPlatformViewCreated,
)
: Text('$defaultTargetPlatform not support TextView yet');
}
_onPlatformViewCreated(int id) => widget.watcher(TextController(id));
}
實作 Android 端的代碼
// 需要同 flutter 端一緻
private const val TextType = "com.demo.plugin/textview"
private const val TextChannel = "com.demo.plugin/textview_"
// 展示的 PlatformView
class FlutterTextView(context: Context?, messenger: BinaryMessenger, id: Int)
: PlatformView, MethodCallHandler {
private val textView = TextView(context).apply { gravity = Gravity.CENTER }
private val channel = MethodChannel(messenger, "$TextChannel$id")
init {
channel.setMethodCallHandler(this) // 注冊互動的 MethodChannel
}
override fun getView(): View = textView // 最終傳回的為 textView 執行個體
override fun dispose() {} // textview 無記憶體洩漏情況,是以該方法可空
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"text#setText" -> {
textView.text = call.arguments?.toString() ?: ""
result.success(null)
}
"text#setTextSize" -> {
// dart 的 double 直接轉成 Float 會出錯,通過 String 類型來過渡下即可
textView.textSize = "${call.arguments ?: 12}".toFloat()
result.success(null)
}
else -> result.notImplemented()
}
}
}
// 定義完 PlatformView,則可以實作 PlatformViewFactory
class TextViewFactory(private val messenger: BinaryMessenger)
: PlatformViewFactory(StandardMessageCodec.INSTANCE) {
override fun create(context: Context?, id: Int, `object`: Any?):
PlatformView = FlutterTextView(context, messenger, id) // 傳回 PlatformView 即可
}
// 注冊該 view
class ViewPlugin {
companion object {
@JvmStatic
fun registerWith(registrar: Registrar) {
registrar.platformViewRegistry()
.registerViewFactory(TextType, TextViewFactory(registrar.messenger()))
}
}
}
調用 AndroidView
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('AndroidView'),
),
body: TextView((controller) {
controller.setText("Hello Wrold!!");
controller.setTextSize(50.0);
}),
);
}
}
最終将
Android
TextView
顯示到
Flutter
層,效果圖就不貼了。當然了,這個例子沒有一點實用性,隻是作為一個簡單的例子而已,當遇到
Flutter
缺少原生需要的
View
時候,則可以通過該方法來實作,使用時候注意點參考上面提到的~