Flutter插件(Plugin)开发 - Android视角

前言

上篇文章 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

  • 打开本地音乐:"open"
  • 播放:"start"
  • 暂停:"pause"
  • 获取总时长:"getDuration"

上述接口都由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端

照例咱们先来看看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。换句话说,你的插件里的那些个MethodChannelEventChannel都是经过这个函数注册到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中我会用startActivityForResultonActivityResult来获取音频文件,但是咱们如今开发的是一个插件,不是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端怎么作。

插件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);
  }
}

复制代码

插件Example App

除了自身的逻辑以外,一个插件还要有示例应用来演示其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.mdCHANGELOG.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插件包,并且是开源的。你们感兴趣的话能够学习一下官方是如何开发插件的。

相关文章
相关标签/搜索