背景
前面我們講了很多 Flutter 相關的知識點,但是我們並沒有介紹怎樣實現 Flutter 與原生的通信。
比如我在 Flutter UI 上面點擊了一個按鈕,我希望原生做一些處理,那么原生怎么知道?
比如我在原生有些變化需要告知 Flutter,Flutter 又如何獲知?
本篇我們先解決第一個問題。即 Flutter-> 原生的通信。
路由回顧
之前我們一直在講 Flutter 相關的知識點,而且基本上都是在 main.dart 文件上面折騰,為了避免很多小伙伴覺得我們跨度過大。
因此我們這里補充一下之前第三篇 Flutter 即學即用系列博客——03 在舊有項目引入 Flutter 的知識點。
在 Flutter Module 的 main.dart 文件里面,對於存在多個頁面的情況,我們可以寫下面的模板代碼:
import 'dart:ui';
import 'package:flutter/material.dart';
void main() => runApp(_widgetForRoute(window.defaultRouteName));
Widget _widgetForRoute(String route) {
switch (route) {
case 'route1':
return SomeWidget(...);
case 'route2':
return SomeOtherWidget(...);
default:
return Center(
child: Text('Unknown route: $route', textDirection: TextDirection.ltr),
);
}
}
這段代碼我們可以重點關注 switch 那一塊代碼。這里會根據不同的路由,返回不同的頁面。
下面我們會用到這種寫法。
實際案例
接下來我們通過實際案例來說明如何實現 Flutter 向原生發送消息?
我們的案例是假設我要獲取 Android 設備的當前電量,我希望點擊按鈕之后電量會顯示出來。
當然這里的按鈕和顯示電量的文本都是 Flutter 界面的。
那么步驟是怎樣的呢?
1. 搭建 Flutter 界面
我們將界面寫成一個單獨的 battery_widget.dart 文件:
import 'package:flutter/material.dart';
class BatteryWidget extends StatefulWidget {
@override
_BatteryWidgetState createState() => _BatteryWidgetState();
}
class _BatteryWidgetState extends State<BatteryWidget> {
String _batteryLevel = 'Battery level: unknown.';
void _getBatteryLevel() {}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(_batteryLevel),
RaisedButton(
child: const Text('Refresh'),
onPressed: _getBatteryLevel,
),
],
),
);
}
}
很簡單的界面,就是一個文本和一個按鈕,排成一列。
然后我們 main.dart 修改如下:
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:my_flutter/battery_widget.dart';
void main() => runApp(_widgetForRoute(window.defaultRouteName));
Widget _widgetForRoute(String route) {
switch (route) {
case 'battery':
return MaterialApp(
home: Scaffold(
body: BatteryWidget(),
),
);
default:
return MaterialApp(
home: Scaffold(
body: Container(),
),
);
}
}
這里的關鍵點是指定 route 名字為 battery 時,返回我們剛剛新建的 battery_widget 界面。
2. 原生調用 Flutter 界面
在 MainActivity.java 里面,我們寫出下面代碼:
package com.nesger.flutterdemo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import io.flutter.facade.Flutter;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View flutterView = Flutter.createView(
MainActivity.this,
getLifecycle(),
"battery"
);
FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
addContentView(flutterView, layout);
}
}
可以看到 battery 指定了要加載的 Flutter 界面。
運行后效果如下:
接下來就是關鍵的在點擊按鈕的時候如何獲取原生設備電量。
根據上面的代碼,我們知道點擊按鈕會執行 _getBatteryLevel 方法。因此我們要在這里做一些修改。
3. Flutter 定義 MethodChannel
我們在 _BatteryWidgetState 里面加入下面變量:
static const MethodChannel methodChannel = MethodChannel('samples.flutter.io/battery');
samples.flutter.io/battery 可以自己指定,一般保證唯一,所以 samples 實際使用可以替換為包名。主要是要跟原生對應即可。
4. Flutter 調用 methodChannel API invokeMethod 調用原生某個方法並獲取對應的值。
final int result = await methodChannel.invokeMethod('getBatteryLevel');
比如我們這里要通過原生的 getBatteryLevel 方法獲取到對應的電量,並將返回值用 result 保存。
這里的 await 是因為這個操作是異步的。同時 _getBatteryLevel 也要改為對應的異步方法,因此最終方法代碼如下:
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await methodChannel.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level: $result%.';
} on PlatformException {
batteryLevel = 'Failed to get battery level.';
}
setState(() {
_batteryLevel = batteryLevel;
});
}
可以看到通過異步方法獲取到電量之后通過 setState 方法更新界面。
5. 原生定義 MethodChannel
private static final String BATTERY_CHANNEL = "samples.flutter.io/battery";
注意需要跟 Flutter 的一一對應。
6. 原生調用創建 MethodChannel 並通過 MethodCallHandler 接收 Flutter 的方法調用
new MethodChannel((FlutterView)flutterView, BATTERY_CHANNEL).setMethodCallHandler(
new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
if (call.method.equals("getBatteryLevel")) {
int batteryLevel = getBatteryLevel();
if (batteryLevel != -1) {
result.success(batteryLevel);
} else {
result.error("UNAVAILABLE", "Battery level not available.", null);
}
} else {
result.notImplemented();
}
}
}
);
可以看到我們是通過 call.method 來區分 Flutter 的不同方法調用。
這里 result.success 返回成功回調。 result.error 返回錯誤回調。result.notImplemented 表明沒有對應實現。
最后我們實現原生 getBatteryLevel 方法即可。
如下:
private int getBatteryLevel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
} else {
Intent intent = new ContextWrapper(getApplicationContext()).
registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
return (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
}
}
運行點擊按鈕,效果如下:
到此我們 Flutter 調用原生並獲取返回值的方法就介紹完了。
這里我們總結如下:
Flutter 准備工作:
- 定義 MethodChannel
- 通過異步方法調用 methodChannel 的 invokeMethod 指定這個 methodChannel 具體要調用的方法名
原生准備工作:
- 定義 CHANNEL(與 Flutter 對應)
- 創建 MethodChannel 並通過 setMethodCallHandler 方法來區分 Flutter 的不同調用方法名和返回對應的回調
源碼位置:
https://github.com/nesger/FlutterSample/tree/feature/method_channel
參考鏈接:
https://flutter.dev/docs/development/platform-integration/platform-channels
https://github.com/flutter/flutter/tree/master/examples/platform_channel
更多閱讀:
Flutter 即學即用系列博客
Flutter 即學即用系列博客——01 環境搭建
Flutter 即學即用系列博客——02 一個純 Flutter Demo 說明
Flutter 即學即用系列博客——03 在舊有項目引入 Flutter
Flutter 即學即用系列博客——04 Flutter UI 初窺
Flutter 即學即用系列博客——05 StatelessWidget vs StatefulWidget
Flutter 即學即用系列博客——06 超實用 Widget 集錦
Flutter 即學即用系列博客——07 RenderFlex overflowed 引發的思考
Flutter & dart
dart 如何優雅的避空
Flutter map 妙用及 .. 使用