上篇文章 Flutter如何和Native通訊-Android視角 講了Flutter app和Native通訊的機制。文末提到若是你把某個Native功能(好比藍牙,GPS什麼的)用Platform Channels包裝成了完美的Flutter API。那麼你能夠用插件(Plugin)的形式把你的API開放給Flutter開發者們使用。html
Flutter裏的包分爲插件包(Plugin packages)和Dart包(Dart packages)的區別。java
-- 插件包(Plugin packages)是當你須要暴露Native API給別人的時候使用的形式,內部須要使用Platform Channels幷包含Androiod/iOS原生邏輯。android
-- Dart包(Dart packages)是當你須要開發一個純Dart組件(好比一個自定義的Weidget)的時候使用的形式,內部沒有Native代碼。ios
本文會簡單介紹一下怎麼從零開始開發一個包裝了Android MediaPlayer
的Flutter插件。相關代碼能夠從Github獲取。git
注意,此插件不包含iOS相關代碼,而且只有有限的功能,僅供學習使用,切勿用於正式App開發。github
先上一張圖說明一下使用場景。 app
使用這個插件的Flutter App能夠實現一個有如下功能的低配版音樂播放器。async
有了以上需求,那咱們來考慮插件須要給Flutter App提供哪些接口:ide
上述接口都由Flutter app發起調用,須要MethodChannel
實現。此外,插件還須要上報播放器狀態和播放時長,上報這類事件由EventChannel
實現。函數
需求搞清楚了,那咱們就開始開發這個插件吧。
首先在Android Studio裏新建一個Flutter Plugin工程: File > New > New Flutter Project... 在彈出的對話框裏選擇 "Flutter Plugin"
而後一路 "Next"下去。完成後的工程結構以下: 整個工程包含4個主目錄,android和ios目錄下是對應Native代碼。lib目錄下是插件的Flutter端代碼。example目錄下是個完整的Flutter App。這個App示範怎麼使用你開發的Flutter插件。在本例中,example在手機上跑起來就是上面那個播放器的樣子。照例咱們先來看看Native端怎麼作,在android目錄下,IDE會爲你生成一個XXXPlugin.java的文件。打開打開之後能夠看到下面這樣的示例代碼:
/** FlutterMusicPlugin */
public class FlutterMusicPlugin implements MethodCallHandler {
/** Plugin registration. */
public static void registerWith(Registrar registrar) {
final FlutterMusicPlugin plugin = new FlutterMusicPlugin();
final MethodChannel channel = new MethodChannel(registrar.messenger(), "flutter_music_plugin");
channel.setMethodCallHandler(plugin);
}
@Override
public void onMethodCall(MethodCall call, Result result) {
// TODO implement method call handler
}
}
複製代碼
裏面有一個實現了MethodCallHandler
的類FlutterPlugin
和一個靜態函數registerWith
。在這個靜態函數裏,new了一個MethodChannel
,而後把FlutterPlugin的實例設置給了這個MehodChannel。換句話說,你的插件裏的那些個MethodChannel
,EventChannel
都是經過這個函數註冊到Host App的。這樣Flutter端在調用的時候才能找到對應的channel。接下來咱們要作的就是重寫onMethodCall
這個函數,把以前定義好的媒體播放的API在這裏作路由:
@Override
public void onMethodCall(MethodCall call, Result result) {
switch (call.method) {
case "pause":
// 暫停
mMediaPlayer.pause();
break;
case "start":
// 開始播放
mMediaPlayer.start();
break;
case "open":
//TODO 打開本地音頻文件
break;
case "getDuration":
// 獲取音頻時長
if (mMediaPlayer != null) {
result.success(mMediaPlayer.getDuration());
} else {
result.error("ERROR", "no valid media player", null);
}
break;
default:
result.notImplemented();
break;
}
}
複製代碼
具體本地MediaPlayer的操做就不細說了,你們能夠去看源碼。MethodChannel就添加完了。此外咱們還須要上報播放器的狀態和播放時的進度,這就須要在registerWith
裏再註冊兩個EventChannel了
public static void registerWith(Registrar registrar) {
...
// 上報播放器的狀態的EventChannel
EventChannel status_channel = new EventChannel(registrar.messenger(), "flutter_music_plugin.event.status");
status_channel.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object o, EventChannel.EventSink eventSink) {
// 把eventSink存起來
plugin.setStateSink(eventSink);
}
@Override
public void onCancel(Object o) {
}
});
//上報播放進度的EventChannel
EventChannel position_channel = new EventChannel(registrar.messenger(), "flutter_music_plugin.event.position");
position_channel.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object o, EventChannel.EventSink eventSink) {
// 把eventSink存起來
plugin.setPositionSink(eventSink);
}
@Override
public void onCancel(Object o) {
}
});
}
複製代碼
註冊完之後咱們就拿到了兩個EventSink
,當須要的時候就能夠用須要的EventSink
給Flutter App上報事件了。
Native這邊還有一環是打開本地音頻文件的操做,這裏我偷個懶,用發送Intent
的方式來讓用戶在第三方app中選擇音頻文件。若是是在Activity中我會用startActivityForResult
和onActivityResult
來獲取音頻文件,但是咱們如今開發的是一個插件,不是Activity怎麼辦?
回想一下咱們用來註冊插件的靜態函數registerWith
,入參的類型是Registrar
。看看它裏面都有啥?
public interface Registrar {
//返回 Host app的Activity
Activity activity();
//返回 Application Context.
Context context();
//返回 活動Context
Context activeContext();
//返回 BinaryMessenger 主要用來註冊Platform channels
BinaryMessenger messenger();
//返回 TextureRegistry,從裏面能夠拿到SurfaceTexture
TextureRegistry textures();
//返回 當前Host app建立的FlutterView
FlutterView view();
//返回Asset對應的文件路徑
String lookupKeyForAsset(String var1);
//返回Asset對應的文件路徑
String lookupKeyForAsset(String var1, String var2);
//插件對外發布的一個"值"
PluginRegistry.Registrar publish(Object var1);
//註冊權限相關的回調
PluginRegistry.Registrar addRequestPermissionsResultListener(PluginRegistry.RequestPermissionsResultListener var1);
//註冊ActivityResult回調
PluginRegistry.Registrar addActivityResultListener(PluginRegistry.ActivityResultListener var1);
//註冊NewIntent回調
PluginRegistry.Registrar addNewIntentListener(PluginRegistry.NewIntentListener var1);
//註冊UserLeaveHint回調
PluginRegistry.Registrar addUserLeaveHintListener(PluginRegistry.UserLeaveHintListener var1);
//註冊View銷燬回調
PluginRegistry.Registrar addViewDestroyListener(PluginRegistry.ViewDestroyListener var1);
}
複製代碼
。。。簡直就是個寶庫啊。裏面的中文註釋我是照官方英文文檔翻譯的,有些方法的用途也不太明確,有待你們的發掘。本例中目前只須要兩個方法,調用activity()
就拿到Host App的Activity。addActivityResultListener
設置處理返回結果的回調。代碼以下:
// 實現 PluginRegistry.ActivityResultListener
public class FlutterMusicPlugin implements MethodCallHandler, PluginRegistry.ActivityResultListener {
...
private Activity mActivity;
// 加個構造函數,入參是Activity
private FlutterMusicPlugin(Activity activity) {
// 存起來
mActivity = activity;
}
public static void registerWith(Registrar registrar) {
//傳入Activity
final FlutterMusicPlugin plugin = new FlutterMusicPlugin(registrar.activity());
...
// 註冊ActivityResult回調
registrar.addActivityResultListener(plugin);
}
@Override
public void onMethodCall(MethodCall call, Result result) {
switch (call.method) {
...
case "open":
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("audio/*");
mActivity.startActivityForResult(intent, REQUEST_CODE_OPEN);
break;
...
}
}
@Override
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_OPEN && resultCode == RESULT_OK) {
Uri uri = data.getData();
if (uri != null) {
// 拿到音頻文件uri,開始播放。
play(uri);
} else {
mStateSink.error("ERROR", "invalid media file", null);
}
return true;
}
return false;
}
}
複製代碼
咱們改造一下FlutterMusicPlugin
, 增長以Activity爲入參的構造函數,在靜態函數registerWith
裏實例化的時候傳入Host app的Activity。同時註冊自身來處理onActivityResult
回調。 在onMethodCall
方法內"open"下啓動第三方選擇音頻文件的頁面。當用戶選好了某首歌返回的時候,插件這邊就會拿到音頻文件uri,並開始播放。
至此,Native端的邏輯就完成了,咱們再來看看插件的Flutter端怎麼作。
IDE在lib目錄下會幫你自動生成flutter_music_plugin.dart文件,這個就是插件的Flutter代碼所在了,內容比較簡單,就是對咱們定義好的Platform channels的包裝。直接上代碼:
typedef void EventHandler(Object event);
class FlutterMusicPlugin {
static const MethodChannel _channel = const MethodChannel('flutter_music_plugin');
static const EventChannel _status_channel = const EventChannel('flutter_music_plugin.event.status');
static const EventChannel _position_channel = const EventChannel('flutter_music_plugin.event.position');
static Future<void> open() async {
await _channel.invokeMethod('open');
}
static Future<void> pause() async {
await _channel.invokeMethod('pause');
}
static Future<void> start() async {
await _channel.invokeMethod('start');
}
static Future<Duration> getDuration() async {
int duration = await _channel.invokeMethod('getDuration');
return Duration(milliseconds: duration);
}
static listenStatus(EventHandler onEvent, EventHandler onError) {
_status_channel.receiveBroadcastStream().listen(onEvent, onError: onError);
}
static listenPosition(EventHandler onEvent, EventHandler onError) {
_position_channel.receiveBroadcastStream().listen(onEvent, onError: onError);
}
}
複製代碼
除了自身的邏輯以外,一個插件還要有示例應用來演示其API怎麼使用,同時,示例應用也是咱們開發,調試,驗證插件的必備工具。本例中的示例可參考example目錄下的main.dart文件。使用插件API的主要邏輯都在State
中。簡要代碼以下
@override
void initState() {
super.initState();
// 在這裏註冊EventChannles,參數傳入響應的回調
FlutterMusicPlugin.listenStatus(_onPlayerStatus, _onPlayerStatusError);
FlutterMusicPlugin.listenPosition(_onPosition, _onPlayerStatusError);
}
...
// 根據播放狀態調用pause或start
void _playPause() {
switch (_status) {
case "started":
FlutterMusicPlugin.pause();
break;
case "paused":
case "completed":
FlutterMusicPlugin.start();
break;
}
}
// 打開媒體文件
void _open() {
FlutterMusicPlugin.open();
}
// MediaPlayer出錯事件處理
void _onPlayerStatusError(Object event) {
print(event);
}
// MediaPlayer狀態改變事件處理
void _onPlayerStatus(Object event) {
setState(() {
_status = event;
});
if (_status == "started") {
_getDuration();
}
}
// 獲取音頻時長
void _getDuration() async {
Duration duration = await FlutterMusicPlugin.getDuration();
setState(() {
_duration = duration;
});
}
// 播放進度事件處理
void _onPosition(Object event) {
Duration position = Duration(milliseconds: event);
setState(() {
_position = position;
});
}
複製代碼
當你的插件開發測試完成之後,你就能夠把你的插件發佈出去了。 發佈以前,先檢查pubspec.yaml
, README.md
和CHANGELOG.md
這幾個文件的內容是否完整正確。而後運行下面這個命令檢查插件是否能夠發佈。
$ flutter packages pub publish --dry-run
若是有問題存在的話,會在終端輸出相關信息,你須要據此作出修改直到返回成功。具體遇到的問題能夠參考官方文檔
最後去掉--dry-run
之後再運行以上命令。
$ flutter packages pub publish
恭喜你,你的插件終於發佈出去了。
從前文開發插件的過程當中咱們知道了在插件Android代碼裏有一個靜態函數registerWith
,這個函數能夠把插件註冊到Host App。那麼問題來了,插件是何時註冊的呢?這個靜態函數是被誰調用的呢? 答案就在example app的MainActivity
裏:
public class MainActivity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 插件在這裏註冊
GeneratedPluginRegistrant.registerWith(this);
}
}
複製代碼
在onCreate
函數裏,有這麼一行代碼GeneratedPluginRegistrant.registerWith(this)
。插件就是在這裏註冊的。再看看GeneratedPluginRegistrant
的內容就明白了:
public final class GeneratedPluginRegistrant {
public static void registerWith(PluginRegistry registry) {
if (alreadyRegisteredWith(registry)) {
return;
}
//那個註冊的靜態函數是在這裏被調用的
FlutterMusicPlugin.registerWith(registry.registrarFor("io.github.zhangjianli.fluttermusicplugin.FlutterMusicPlugin"));
}
private static boolean alreadyRegisteredWith(PluginRegistry registry) {
final String key = GeneratedPluginRegistrant.class.getCanonicalName();
if (registry.hasPlugin(key)) {
return true;
}
registry.registrarFor(key);
return false;
}
}
複製代碼
在第一個靜態函數裏就找到了調用插件的registerWith
函數的地方。這個類是IDE幫咱們自動生成的。也就是說,插件的註冊徹底不須要開發者去幹預。
本文經過開發一個音樂播放功能的插件簡要介紹了Flutter插件包的開發過程。整體來說,插件的開發過程並非很複雜,關鍵的問題仍是在可否抹平Android和iOS平臺差別上面。另外,Flutter官方維護了一批Flutter插件包,並且是開源的。你們感興趣的話能夠學習一下官方是如何開發插件的。