背景
前面我們講了很多 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,
),
],
),
);
}
}
/<widget>/<batterywidget>
很簡單的界面,就是一個文本和一個按鈕,排成一列。
然後我們 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;
});
}
/<void>
可以看到通過異步方法獲取到電量之後通過 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
閱讀更多 安卓小煜 的文章