Flutter的愿景是一般的开发者只需要开发Flutter代码就能实现跨平台的应用,官方提供了一些插件,也有很多可以可以直接拿来使用的第三方插件。
但是现实是现实,例如当遇到定制化的功能时,编写插件是不可避免的。譬如我们有一个自定义协议的蓝牙功能,这个功能在Flutter中就不可能直接拿来使用了,需要编写插件让Flutter进行调用。本文我们将来看看Flutter插件是如何实现的。
前言
本文我们用Flutter来仿写网易云音乐的播放页面的功能,其中音乐的播放,音乐的暂停,快进,音乐的时长获取,音乐播放的进度等功能我们需要用原生代码编写插件来实现。
提示:本文用音乐播放器的插件只是为了提供一个编写Flutter插件的思路和方法,当需要自己编写插件的时候可以方便的来实现。播放音视频的Flutter插件已经有一些优秀的三方库已经实现了。
说明:
- 由于是音频播放,我制作GIF的时候没法体现音乐元素,所以音乐只能我自己独自欣赏了,哈哈~~
- 本文先只介绍iOS的插件制作,下篇文章我们再来介绍Android的插件制作。
架构概览
我们从上面的官方架构图可以看出,Flutter和Native代码是通过MethodChannel
进行通信的。
Flutter端向iOS端发送消息
Flutter端的代码
- 创建一个播放器类
AudioPlayer
, 然后定义为单例模式
class AudioPlayer {
// 单例
factory AudioPlayer() => _getInstance();
static AudioPlayer get instance => _getInstance();
static AudioPlayer _instance;
AudioPlayer._internal() {}
static AudioPlayer _getInstance() {
if (_instance == null) {
_instance = new AudioPlayer._internal();
}
return _instance;
}
}
- 创建播放器的
MethodChannel
class AudioPlayer {
static final channel = const MethodChannel("netmusic.com/audio_player");
}
MethodChannel
名字要有意义,其组成遵循"域名"+"/"+"功能",随意写就显得不够专业。
- 通过
MethodChannel
的invokeMethod
实现播放音乐
/// 播放
Future play() async {
final result = await channel.invokeMethod("play", {'url': audioUrl});
return result ?? 0;
}
play
就是方法名,{'url': audioUrl}
就是参数invokeMethod
是异步的,所以返回值需要用Future
包裹。
- 通过
MethodChannel
的invokeMethod
实现暂停音乐
/// 暂停
Future pause() async {
final result = await channel.invokeMethod("pause", {'url': audioUrl});
return result ?? 0;
}
- 通过
MethodChannel
的invokeMethod
实现继续播放音乐
/// 继续播放
Future resume() async {
final result = await channel.invokeMethod("resume", {'url': audioUrl});
return result ?? 0;
}
- 通过
MethodChannel
的invokeMethod
实现拖动播放位置
/// 拖动播放位置
Future seek(int time) async {
final result = await channel.invokeMethod("seek", {
'position': time,
});
return result ?? 0;
}
iOS端的代码
前提:需要用Xcode打开iOS项目,这是开始编写的基础。
- 创建一个播放器类
PlayerWrapper
class PlayerWrapper: NSObject {
var vc: FlutterViewController
var channel: FlutterMethodChannel
var player: AVPlayer?
}
- 在
AppDelegate
中初始化PlayerWrapper
,并将FlutterViewController
作为初始化参数。
@objc class AppDelegate: FlutterAppDelegate {
// 持有播放器
var playerWrapper: PlayerWrapper?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 初始化播放器
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
playerWrapper = PlayerWrapper(vc: controller)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
- FlutterAppDelegate的根视图就是一个FlutterViewController,这个我们在以前的文章中有介绍;
- FlutterViewController中有一个FlutterBinaryMessenger,创建
FlutterMethodChannel
时需要,所以将其传入PlayerWrapper
。
- 创建播放器的
FlutterMethodChannel
class PlayerWrapper: NSObject {
init(vc: FlutterViewController) {
self.vc = vc
channel = FlutterMethodChannel(name: "netmusic.com/audio_player", binaryMessenger: vc.binaryMessenger)
super.init()
}
}
name
的值必须和Flutter中的对应,否则是没法通信的;binaryMessenger
就使用FlutterViewController的FlutterBinaryMessenger,前面提到过。
- 接收Flutter端的调用,然后回调Flutter端播放进度和结果等。
由于是被动接收,所以可以想象的实现是注册一个回调函数,接收Flutter端的调用方法和参数。
init(vc: FlutterViewController) {
//...
channel.setMethodCallHandler(handleFlutterMessage);
}
// 从Flutter传过来的方法
public func handleFlutterMessage(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
// 1. 获取方法名和参数
let method = call.method
let args = call.arguments as? [String: Any]
if method == "play" {
// 2.1 确保有url参数
guard let url = args?["url"] as! String? else {
result(0)
return
}
player?.pause()
// 2.2 确保有url参数正确
guard let audioURL = URL.init(string: url) else {
result(0)
return
}
// 2.3 根据url初始化播放内容,然后开始进行播放
let asset = AVAsset.init(url: audioURL)
let item = AVPlayerItem.init(asset: asset);
player = AVPlayer(playerItem: item);
player?.play();
// 2.4 定时检测播放进度
player?.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 1), queue: nil, using: { [weak self] (time) in
// *********回调Flutter当前播放进度*********
self?.channel.invokeMethod("onPosition", arguments: ["value": time.value / Int64(time.timescale)])
})
keyVakueObservation?.invalidate()
// 2.5 监测播放状态
keyVakueObservation = item.observe(\AVPlayerItem.status) { [weak self] (playerItem, change) in
let status = playerItem.status
if status == .readyToPlay {
// *********回调Flutter当前播放内容的总长度*********
if let time = self?.player?.currentItem?.asset.duration {
self?.channel.invokeMethod("onDuration", arguments: ["value": time.value / Int64(time.timescale)])
}
} else if status == .failed {
// *********回调Flutter当前播放出现错误*********
self?.channel.invokeMethod("onError", arguments: ["value": "pley failed"])
}
}
// 2.6 监测播放完成
notificationObservation = NotificationCenter.default.addObserver(
forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: item,**
queue: nil
) {
[weak self] (notification) in
self?.channel.invokeMethod("onComplete", arguments: [])
}**
result(1)
} else if method == "pause" || method == "stop" {
// 3 暂停
player?.pause()
result(1)
} else if method == "resume" {
// 4 继续播放
player?.play()
result(1)
} else if method == "seek" {
guard let position = args?["position"] as! Int? else {
result(0)
return
}
// 4 拖动到某处进行播放
let seekTime: CMTime = CMTimeMake(value: Int64(position), timescale: 1)
player?.seek(to: seekTime);
}
}
handleFlutterMessage
这个回调函数有两个参数:FlutterMethodCall
接收Flutter传过来的方法名method
和参数arguments
,FlutterResult
可以返回调用的结果,例如result(1)
就给Flutter返回了1
这个结果。- 获取到
FlutterMethodCall
的方法名和参数后就可以进行处理了,我们以play
为例:
- 根据url初始化播放内容,然后开始进行播放;
- 通过
player.addPeriodicTimeObserver
方法检测播放进度,然后通过FlutterMethodChannel
的invokeMethod
方法传递当前的进度给Flutter端,方法名是onPosition
,参数是当前进度;- 后面还有一列逻辑:例如监听播放状态,监听播放完成等。
目前为止,iOS端的代码完成了。接下来就是Flutter端接收iOS端的方法和参数了。
Flutter端接收iOS端发送的消息
iOS端向Flutter端发送了onPosition
(当前播放进度),onComplete
(播放完成),onDuration
(当前歌曲的总长度)和onError
(播放出现错误)等几个方法调用。
- Flutter端注册回调
AudioPlayer._internal() {
channel.setMethodCallHandler(nativePlatformCallHandler);
}
/// Native主动调用的方法
Future nativePlatformCallHandler(MethodCall call) async {
try {
// 获取参数
final callArgs = call.arguments as Map;
print('nativePlatformCallHandler call ${call.method} $callArgs');
switch (call.method) {
case 'onPosition':
final time = callArgs['value'] as int;
_currentPlayTime = time;
_currentPlayTimeController.add(_currentPlayTime);
break;
case 'onComplete':
this.updatePlayerState(PlayerState.COMPLETED);
break;
case 'onDuration':
final time = callArgs['value'] as int;
_totalPlayTime = time;
_totalPlayTimeController.add(totalPlayTime);
break;
case 'onError':
final error = callArgs['value'] as String;
this.updatePlayerState(PlayerState.STOPPED);
_errorController.add(error);
break;
}
} catch (ex) {
print('Unexpected error: $ex');
}
}
- 注册回调也是使用
setMethodCallHandler
方法,MethodCall
对应的也包含方法名和参数;- 获取到对应的数据后Flutter就可进行数据的展示了。
- Flutter端对数据的更新
我们以onDuration
(当前歌曲的总长度)为例进行介绍。
class AudioPlayer {
// 1. 记录下总时间
int _totalPlayTime = 0;
int get totalPlayTime => _totalPlayTime;
// 2. 代表歌曲时长的流
final StreamController _totalPlayTimeController =
StreamController.broadcast();
Stream get onTotalTimeChanged => _totalPlayTimeController.stream;
Future nativePlatformCallHandler(MethodCall call) async {
try {
final callArgs = call.arguments as Map;
print('nativePlatformCallHandler call ${call.method} $callArgs');
switch (call.method) {
// 3. 记录下总时间和推送更新
case 'onDuration':
final time = callArgs['value'] as int;
_totalPlayTime = time;
_totalPlayTimeController.add(totalPlayTime);
break;
}
} catch (ex) {
print('Unexpected error: $ex');
}
}
}
_totalPlayTime
记录下总播放时长;_totalPlayTimeController
是总播放时长的流,当调用add
方法时,onTotalTimeChanged
的监听者就能收到新的值;
- StreamBuilder监听流的数据
StreamBuilder(
initialData: "00:00",
stream: AudioPlayer().onTotalTimeChanged,
builder: (context, snapshot) {
if (!snapshot.hasData)
return Text(
"00:00",
style: TextStyle(color: Colors.white70),
);
return Text(
AudioPlayer().totalPlayTimeStr,
style: TextStyle(color: Colors.white70),
);
},
),
监听
AudioPlayer().onTotalTimeChanged
的数据变化,然后最新的值展示在Text
上。
代码
audio_player.dart
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:netmusic_flutter/music_item.dart';
class AudioPlayer {
// 定义一个MethodChannel
static final channel = const MethodChannel("netmusic.com/audio_player");
// 单例
factory AudioPlayer() => _getInstance();
static AudioPlayer get instance => _getInstance();
static AudioPlayer _instance;
AudioPlayer._internal() {
// 初始化
channel.setMethodCallHandler(nativePlatformCallHandler);
}
static AudioPlayer _getInstance() {
if (_instance == null) {
_instance = new AudioPlayer._internal();
}
return _instance;
}
// 播放状态
PlayerState _playerState = PlayerState.STOPPED;
PlayerState get playerState => _playerState;
// 时间
int _totalPlayTime = 0;
int _currentPlayTime = 0;
int get totalPlayTime => _totalPlayTime;
int get currentPlayTime => _currentPlayTime;
String get totalPlayTimeStr => formatTime(_totalPlayTime);
String get currentPlayTimeStr => formatTime(_currentPlayTime);
// 歌曲
MusicItem _item;
set item(MusicItem item) {
_item = item;
}
String get audioUrl {
return _item != null
? "https://music.163.com/song/media/outer/url?id=${_item.id}.mp3"
: "";
}
Future togglePlay() async {
if (_playerState == PlayerState.PLAYING) {
return pause();
} else {
return play();
}
}
/// 播放
Future play() async {
if (_item == null) return 0;
// 如果是停止状态
if (_playerState == PlayerState.STOPPED ||
_playerState == PlayerState.COMPLETED) {
// 更新状态
this.updatePlayerState(PlayerState.PLAYING);
final result = await channel.invokeMethod("play", {'url': audioUrl});
return result ?? 0;
} else if (_playerState == PlayerState.PAUSED) {
return resume();
}
return 0;
}
/// 继续播放
Future resume() async {
// 更新状态
this.updatePlayerState(PlayerState.PLAYING);
final result = await channel.invokeMethod("resume", {'url': audioUrl});
return result ?? 0;
}
/// 暂停
Future pause() async {
// 更新状态
this.updatePlayerState(PlayerState.PAUSED);
final result = await channel.invokeMethod("pause", {'url': audioUrl});
return result ?? 0;
}
/// 停止
Future stop() async {
// 更新状态
this.updatePlayerState(PlayerState.STOPPED);
final result = await channel.invokeMethod("stop");
return result ?? 0;
}
/// 播放
Future seek(int time) async {
// 更新状态
this.updatePlayerState(PlayerState.PLAYING);
final result = await channel.invokeMethod("seek", {
'position': time,
});
return result ?? 0;
}
/// Native主动调用的方法
Future nativePlatformCallHandler(MethodCall call) async {
try {
// 获取参数
final callArgs = call.arguments as Map;
print('nativePlatformCallHandler call ${call.method} $callArgs');
switch (call.method) {
case 'onPosition':
final time = callArgs['value'] as int;
_currentPlayTime = time;
_currentPlayTimeController.add(_currentPlayTime);
break;
case 'onComplete':
this.updatePlayerState(PlayerState.COMPLETED);
break;
case 'onDuration':
final time = callArgs['value'] as int;
_totalPlayTime = time;
_totalPlayTimeController.add(totalPlayTime);
break;
case 'onError':
final error = callArgs['value'] as String;
this.updatePlayerState(PlayerState.STOPPED);
_errorController.add(error);
break;
}
} catch (ex) {
print('Unexpected error: $ex');
}
}
// 播放状态
final StreamController _stateController =
StreamController.broadcast();
Stream get onPlayerStateChanged => _stateController.stream;
// Video的时长和当前位置时间变化
final StreamController _totalPlayTimeController =
StreamController.broadcast();
Stream get onTotalTimeChanged => _totalPlayTimeController.stream;
final StreamController _currentPlayTimeController =
StreamController.broadcast();
Stream get onCurrentTimeChanged => _currentPlayTimeController.stream;
// 发生错误
final StreamController _errorController = StreamController();
Stream get onError => _errorController.stream;
// 更新播放状态
void updatePlayerState(PlayerState state, {bool stream = true}) {
_playerState = state;
if (stream) {
_stateController.add(state);
}
}
// 这里需要关闭流
void dispose() {
_stateController.close();
_currentPlayTimeController.close();
_totalPlayTimeController.close();
_errorController.close();
}
// 格式化时间
String formatTime(int time) {
int min = (time ~/ 60);
int sec = time % 60;
String minStr = min < 10 ? "0$min" : "$min";
String secStr = sec < 10 ? "0$sec" : "$sec";
return "$minStr:$secStr";
}
}
/// 播放状态
enum PlayerState {
STOPPED,
PLAYING,
PAUSED,
COMPLETED,
}
AppDelegate.swift
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
var playerWrapper: PlayerWrapper?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 播放器
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
playerWrapper = PlayerWrapper(vc: controller)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
PlayerWrapper.swift
import Foundation
import Flutter
import AVKit
import CoreMedia
class PlayerWrapper: NSObject {
var vc: FlutterViewController
var channel: FlutterMethodChannel
var player: AVPlayer?
var keyVakueObservation: NSKeyValueObservation?
var notificationObservation: NSObjectProtocol?
init(vc: FlutterViewController) {
self.vc = vc
channel = FlutterMethodChannel(name: "netmusic.com/audio_player", binaryMessenger: vc.binaryMessenger)
super.init()
channel.setMethodCallHandler(handleFlutterMessage);
}
// 从Flutter传过来的方法
public func handleFlutterMessage(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let method = call.method
let args = call.arguments as? [String: Any]
if method == "play" {
guard let url = args?["url"] as! String? else {
NSLog("无播放地址")
result(0)
return
}
player?.pause()
guard let audioURL = URL.init(string: url) else {
NSLog("播放地址错误")
result(0)
return
}
let asset = AVAsset.init(url: audioURL)
let item = AVPlayerItem.init(asset: asset);
player = AVPlayer(playerItem: item);
player?.play();
player?.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 1), queue: nil, using: { [weak self] (time) in
self?.channel.invokeMethod("onPosition", arguments: ["value": time.value / Int64(time.timescale)])
})
keyVakueObservation?.invalidate()
keyVakueObservation = item.observe(\AVPlayerItem.status) { [weak self] (playerItem, change) in
let status = playerItem.status
if status == .readyToPlay {
if let time = self?.player?.currentItem?.asset.duration {
self?.channel.invokeMethod("onDuration", arguments: ["value": time.value / Int64(time.timescale)])
}
} else if status == .failed {
self?.channel.invokeMethod("onError", arguments: ["value": "pley failed"])
}
}
notificationObservation = NotificationCenter.default.addObserver(
forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: item,
queue: nil
) {
[weak self] (notification) in
self?.channel.invokeMethod("onComplete", arguments: [])
}
result(1)
} else if method == "pause" || method == "stop" {
player?.pause()
result(1)
} else if method == "resume" {
player?.play()
result(1)
} else if method == "seek" {
guard let position = args?["position"] as! Int? else {
NSLog("无播放时间")
result(0)
return
}
let seekTime: CMTime = CMTimeMake(value: Int64(position), timescale: 1)
player?.seek(to: seekTime);
}
}
}
有没有感觉编写插件其实也很简单,附上所有Flutter代码,下篇介绍Android的插件编写。