????????关注后回复 “进群” ,拉你进程序员交流群????????

转自:掘金 chonglingliu

https://juejin.cn/post/6960556761262587918

Flutter的愿景是一般的开发者只需要开发Flutter代码就能实现跨平台的应用,官方提供了一些插件,也有很多可以可以直接拿来使用的第三方插件。

但是现实是现实,例如当遇到定制化的功能时,编写插件是不可避免的。譬如我们有一个自定义协议的蓝牙功能,这个功能在Flutter中就不可能直接拿来使用了,需要编写插件让Flutter进行调用。本文我们将来看看Flutter插件是如何实现的。

前言

本文我们用Flutter来仿写网易云音乐的播放页面的功能,其中音乐的播放音乐的暂停快进音乐的时长获取音乐播放的进度等功能我们需要用原生代码编写插件来实现。

图片较大,截图处理

提示:本文用音乐播放器的插件只是为了提供一个编写Flutter插件的思路和方法,当需要自己编写插件的时候可以方便的来实现。播放音视频的Flutter插件已经有一些优秀的三方库已经实现了。

说明:

  1. 由于是音频播放,我制作GIF的时候没法体现音乐元素,所以音乐只能我自己独自欣赏了,哈哈~~

  2. 本文先只介绍iOS的插件制作,下篇文章我们再来介绍Android的插件制作。

架构概览

我们从上面的官方架构图可以看出,FlutterNative代码是通过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名字要有意义,其组成遵循"域名"+"/"+"功能",随意写就显得不够专业。

  • 通过MethodChannelinvokeMethod实现播放音乐

/// 播放Future<int> play() async {final result = await channel.invokeMethod("play", {'url': audioUrl});return result ?? 0;}
  1. play就是方法名, {'url': audioUrl}就是参数

  2. invokeMethod是异步的,所以返回值需要用Future包裹。

  • 通过MethodChannelinvokeMethod实现暂停音乐

/// 暂停
Future<int> pause() async {final result = await channel.invokeMethod("pause", {'url': audioUrl});return result ?? 0;
}
  • 通过MethodChannelinvokeMethod实现继续播放音乐

/// 继续播放
Future<int> resume() async {final result = await channel.invokeMethod("resume", {'url': audioUrl});return result ?? 0;
}
  • 通过MethodChannelinvokeMethod实现拖动播放位置

/// 拖动播放位置
Future<int> seek(int time) async {final result = await channel.invokeMethod("seek", {'position': time,});return result ?? 0;
}

iOS端的代码

前提:需要用Xcode打开iOS项目,这是开始编写的基础。

  • 创建一个播放器类PlayerWrapper

class PlayerWrapper: NSObject {var vc: FlutterViewControllervar channel: FlutterMethodChannelvar 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! FlutterViewControllerplayerWrapper = PlayerWrapper(vc: controller)GeneratedPluginRegistrant.register(with: self)return super.application(application, didFinishLaunchingWithOptions: launchOptions)}
}
  1. FlutterAppDelegate的根视图就是一个FlutterViewController,这个我们在以前的文章中有介绍;

  2. FlutterViewController中有一个FlutterBinaryMessenger,创建FlutterMethodChannel时需要,所以将其传入PlayerWrapper

  • 创建播放器的FlutterMethodChannel

class PlayerWrapper: NSObject {init(vc: FlutterViewController) {self.vc = vcchannel = FlutterMethodChannel(name: "netmusic.com/audio_player", binaryMessenger: vc.binaryMessenger)super.init()}
}
  1. name的值必须和Flutter中的对应,否则是没法通信的;

  2. binaryMessenger就使用FlutterViewControllerFlutterBinaryMessenger,前面提到过。

  • 接收Flutter端的调用,然后回调Flutter端播放进度和结果等。

由于是被动接收,所以可以想象的实现是注册一个回调函数,接收Flutter端的调用方法和参数。

init(vc: FlutterViewController) {//...channel.setMethodCallHandler(handleFlutterMessage);
}// 从Flutter传过来的方法
public func handleFlutterMessage(_ call: FlutterMethodCall, result: @escaping FlutterResult) {// 1. 获取方法名和参数let method = call.methodlet 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) inlet status = playerItem.statusif 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) inself?.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);}
}
  1. handleFlutterMessage这个回调函数有两个参数:FlutterMethodCall接收Flutter传过来的方法名method和参数arguments, FlutterResult可以返回调用的结果,例如result(1)就给Flutter返回了1这个结果。

  2. 获取到FlutterMethodCall的方法名和参数后就可以进行处理了,我们以play为例:

  • 根据url初始化播放内容,然后开始进行播放;

  • 通过player.addPeriodicTimeObserver方法检测播放进度,然后通过FlutterMethodChannelinvokeMethod方法传递当前的进度给Flutter端,方法名是onPosition,参数是当前进度;

  • 后面还有一列逻辑:例如监听播放状态,监听播放完成等。

目前为止,iOS端的代码完成了。接下来就是Flutter端接收iOS端的方法和参数了。

Flutter端接收iOS端发送的消息

iOS端向Flutter端发送了onPosition(当前播放进度),onComplete(播放完成),onDuration(当前歌曲的总长度)和onError(播放出现错误)等几个方法调用。

  • Flutter端注册回调

AudioPlayer._internal() {channel.setMethodCallHandler(nativePlatformCallHandler);
}/// Native主动调用的方法
Future<void> nativePlatformCallHandler(MethodCall call) async {try {// 获取参数final callArgs = call.arguments as Map<dynamic, dynamic>;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');}
}
  1. 注册回调也是使用setMethodCallHandler方法,MethodCall对应的也包含方法名和参数;

  2. 获取到对应的数据后Flutter就可进行数据的展示了。

  • Flutter端对数据的更新

我们以onDuration(当前歌曲的总长度)为例进行介绍。

class AudioPlayer {// 1. 记录下总时间int _totalPlayTime = 0;int get totalPlayTime => _totalPlayTime;// 2. 代表歌曲时长的流final StreamController<int> _totalPlayTimeController =StreamController<int>.broadcast();Stream<int> get onTotalTimeChanged => _totalPlayTimeController.stream;Future<void> nativePlatformCallHandler(MethodCall call) async {try {final callArgs = call.arguments as Map<dynamic, dynamic>;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');}}
}
  1. _totalPlayTime记录下总播放时长;

  2. _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 {// 定义一个MethodChannelstatic 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<int> togglePlay() async {if (_playerState == PlayerState.PLAYING) {return pause();} else {return play();}}/// 播放Future<int> 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<int> resume() async {// 更新状态this.updatePlayerState(PlayerState.PLAYING);final result = await channel.invokeMethod("resume", {'url': audioUrl});return result ?? 0;}/// 暂停Future<int> pause() async {// 更新状态this.updatePlayerState(PlayerState.PAUSED);final result = await channel.invokeMethod("pause", {'url': audioUrl});return result ?? 0;}/// 停止Future<int> stop() async {// 更新状态this.updatePlayerState(PlayerState.STOPPED);final result = await channel.invokeMethod("stop");return result ?? 0;}/// 播放Future<int> seek(int time) async {// 更新状态this.updatePlayerState(PlayerState.PLAYING);final result = await channel.invokeMethod("seek", {'position': time,});return result ?? 0;}/// Native主动调用的方法Future<void> nativePlatformCallHandler(MethodCall call) async {try {// 获取参数final callArgs = call.arguments as Map<dynamic, dynamic>;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<PlayerState> _stateController =StreamController<PlayerState>.broadcast();Stream<PlayerState> get onPlayerStateChanged => _stateController.stream;// Video的时长和当前位置时间变化final StreamController<int> _totalPlayTimeController =StreamController<int>.broadcast();Stream<int> get onTotalTimeChanged => _totalPlayTimeController.stream;final StreamController<int> _currentPlayTimeController =StreamController<int>.broadcast();Stream<int> get onCurrentTimeChanged => _currentPlayTimeController.stream;// 发生错误final StreamController<String> _errorController = StreamController<String>();Stream<String> 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! FlutterViewControllerplayerWrapper = PlayerWrapper(vc: controller)GeneratedPluginRegistrant.register(with: self)return super.application(application, didFinishLaunchingWithOptions: launchOptions)}
}
PlayerWrapper.swift
import Foundation
import Flutter
import AVKit
import CoreMediaclass PlayerWrapper: NSObject {var vc: FlutterViewControllervar channel: FlutterMethodChannelvar player: AVPlayer?var keyVakueObservation: NSKeyValueObservation?var notificationObservation: NSObjectProtocol?init(vc: FlutterViewController) {self.vc = vcchannel = 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.methodlet 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) inself?.channel.invokeMethod("onPosition", arguments: ["value": time.value / Int64(time.timescale)])})keyVakueObservation?.invalidate()keyVakueObservation = item.observe(\AVPlayerItem.status) { [weak self] (playerItem, change) inlet status = playerItem.statusif 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) inself?.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代码(https://github.com/watchstone/flutter_demos/tree/main/player),下篇介绍Android的插件编写。

-End-

最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!

点击????卡片,关注后回复【面试题】即可获取

在看点这里好文分享给更多人↓↓

Flutter 插件开发:iOS篇相关推荐

  1. flutter 插件开发:分享插件只针对ios平台

    在开发flutter应用的时候总会遇到其他插件解决不了的问题,我遇到的问题就是在ios平台分享时候调起的是系统的分享但是,分享栏里面没有我想要的facebook和twitter平台,所以不得已写了一个 ...

  2. dart和python混编,Flutter与iOS混编(一)

    前言 Flutter和iOS支持两种形式的混编,一种是某一些页面全是用flutter去绘制,另外一只是flutter页面作为iOS某个控制器页面的一部分去展示,后面会逐步去介绍这两种方式的实现 本篇文 ...

  3. Flutter开发IOS,上架AppStore的全部流程以及常遇到的坑

    Flutter开发IOS,上架AppStore的全部流程以及常遇到的坑 本次开发采用的Flutter技术进行开发,没想到会这么快,昨天提交的,今天便已上架appstroe,所以这次来做一次总结,总结从 ...

  4. flutter 控制iOS设备屏幕可旋转支持方向

    场景:flutter开发一个app,非module形式,即:app内部大部分页面是横屏,有部分页面是需要视屏显示(不参与喷子:写一个空控件旋转90度不就好了?但是这样的话状态栏之前的状态,如果你不需要 ...

  5. 开启Fluter基础之旅三-------Material Design风格组件、Cupertino风格组件、Flutter页面布局篇...

    Material Design风格组件: 继续接着上一次https://www.cnblogs.com/webor2006/p/12545701.html的Material Design进行学习. A ...

  6. Flutter 搭建 iOS 命令行服务打包发布全保姆式流程

    theme: smartblue 在以前的 < Android 和 iOS 打包提交审核指南> 里介绍了 Flutter 下打包 Android 和 iOS 的指南,不过这部分内容主要介绍 ...

  7. 集成 jpush-react-native 常见问题汇总 ( iOS 篇)

    给 iOS 应用添加推送功能是一件比较麻烦的事情,本篇文章收集了集成 jpush-react-native 的常见问题,目的是为了帮助用户更好地排查问题 1.收不到推送 确保是在真机上测试,而不是在模 ...

  8. flutter在IOS上的登录实现——QQ登录、微信登录、自动识别手机号一键登录、apple登录

    flutter在IOS上的登录实现--QQ登录.微信登录.自动识别手机号一键登录.apple登录 一.QQ登录 使用的第三方库: 具体操作方法: 1.配置 Universal Links 2.QQ互联 ...

  9. Flutter 发布iOS版本app

    1. 苹果账号和相关证书配置(直接搜索 iOS 证书配置,不多做描述),我这边使用自动生成证书 2. flutter build ios --release // 以创建release版本(flutt ...

  10. Android原生插件开发-开发篇

    原创文档:Android原生插件开发-开发篇 · 语雀 官方文档:原生开发者支持 创建module 点击File=>New=>New Module 选择Android Library,输入 ...

最新文章

  1. LINUX下源码包安装mysql
  2. 人工操作阶段计算机是如何工作的,管理信息系统作业参考答案
  3. 【转】shell pipe与输入输出重定向的区别
  4. Tomcat JVM 初始化加大内存
  5. 低配本用win10服务器系统,低配电脑装win10最早版可以吗
  6. 《异度神剑2》与犹太教卡巴拉略考
  7. 项目管理工具——Jira使用和配置
  8. 什么是QCIF? CIF?2CIF?4CIF?DCIF?
  9. 100个免费可商用字体,你总有一天用到它
  10. 矩阵论——矩阵的标准型
  11. 80004005错误代码_win7系统出现错误代码0x80004005该如何解决
  12. 基因序列 深度学习Deep Learning for Genomics: A Concise Overview
  13. bmp/gif/jpg图象最底层原理分析
  14. MySQL子查询的优缺点_浅谈mysql的子查询
  15. k-选取问题之快速选取策略quickSelection
  16. 解决STM32新增加函数出现Undefined symbol HAL_ADC_Init (referred from main.o). 问题
  17. 智慧物业综合管理系统(java+vue+Uni-app),源码免费分享
  18. 网速测试大师的软件怎么回事,网速测试大师
  19. 使用libcurl库把域名转化IP
  20. Service启动泄露异常:android.app.ServiceConnectionLeaked

热门文章

  1. MySql中设置utf8编码方法
  2. 软件配置管理岗位职责说明
  3. Java 基于mail.jar 和 activation.jar 封装的邮件发送工具类
  4. Wps日期时间格式转文本、科学计数法转数字
  5. vscode之vue文件格式化代码无效怎么办
  6. cmd静默运行_如何在Win10上静默运行批处理文件
  7. 【LTspice】【使用.step命令对LTspice电路进行对比分析】
  8. 计算机怎样保存文件格式,word文档怎样保存为pdf格式
  9. iOS 14.5正式版如约而至 支持通过Apple Watch解锁iPhone
  10. 如何解压war后缀的文件: