Flutter 插件开发:iOS篇
????????关注后回复 “进群” ,拉你进程序员交流群????????
转自:掘金 chonglingliu
https://juejin.cn/post/6960556761262587918
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<int> play() async {final result = await channel.invokeMethod("play", {'url': audioUrl});return result ?? 0;}
play
就是方法名,{'url': audioUrl}
就是参数
invokeMethod
是异步的,所以返回值需要用Future
包裹。
通过
MethodChannel
的invokeMethod
实现暂停音乐
/// 暂停
Future<int> pause() async {final result = await channel.invokeMethod("pause", {'url': audioUrl});return result ?? 0;
}
通过
MethodChannel
的invokeMethod
实现继续播放音乐
/// 继续播放
Future<int> resume() async {final result = await channel.invokeMethod("resume", {'url': audioUrl});return result ?? 0;
}
通过
MethodChannel
的invokeMethod
实现拖动播放位置
/// 拖动播放位置
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)}
}
FlutterAppDelegate的根视图就是一个FlutterViewController,这个我们在以前的文章中有介绍;
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()}
}
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.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);}
}
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<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');}
}
注册回调也是使用
setMethodCallHandler
方法,MethodCall
对应的也包含方法名和参数;获取到对应的数据后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');}}
}
_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 {// 定义一个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篇相关推荐
- flutter 插件开发:分享插件只针对ios平台
在开发flutter应用的时候总会遇到其他插件解决不了的问题,我遇到的问题就是在ios平台分享时候调起的是系统的分享但是,分享栏里面没有我想要的facebook和twitter平台,所以不得已写了一个 ...
- dart和python混编,Flutter与iOS混编(一)
前言 Flutter和iOS支持两种形式的混编,一种是某一些页面全是用flutter去绘制,另外一只是flutter页面作为iOS某个控制器页面的一部分去展示,后面会逐步去介绍这两种方式的实现 本篇文 ...
- Flutter开发IOS,上架AppStore的全部流程以及常遇到的坑
Flutter开发IOS,上架AppStore的全部流程以及常遇到的坑 本次开发采用的Flutter技术进行开发,没想到会这么快,昨天提交的,今天便已上架appstroe,所以这次来做一次总结,总结从 ...
- flutter 控制iOS设备屏幕可旋转支持方向
场景:flutter开发一个app,非module形式,即:app内部大部分页面是横屏,有部分页面是需要视屏显示(不参与喷子:写一个空控件旋转90度不就好了?但是这样的话状态栏之前的状态,如果你不需要 ...
- 开启Fluter基础之旅三-------Material Design风格组件、Cupertino风格组件、Flutter页面布局篇...
Material Design风格组件: 继续接着上一次https://www.cnblogs.com/webor2006/p/12545701.html的Material Design进行学习. A ...
- Flutter 搭建 iOS 命令行服务打包发布全保姆式流程
theme: smartblue 在以前的 < Android 和 iOS 打包提交审核指南> 里介绍了 Flutter 下打包 Android 和 iOS 的指南,不过这部分内容主要介绍 ...
- 集成 jpush-react-native 常见问题汇总 ( iOS 篇)
给 iOS 应用添加推送功能是一件比较麻烦的事情,本篇文章收集了集成 jpush-react-native 的常见问题,目的是为了帮助用户更好地排查问题 1.收不到推送 确保是在真机上测试,而不是在模 ...
- flutter在IOS上的登录实现——QQ登录、微信登录、自动识别手机号一键登录、apple登录
flutter在IOS上的登录实现--QQ登录.微信登录.自动识别手机号一键登录.apple登录 一.QQ登录 使用的第三方库: 具体操作方法: 1.配置 Universal Links 2.QQ互联 ...
- Flutter 发布iOS版本app
1. 苹果账号和相关证书配置(直接搜索 iOS 证书配置,不多做描述),我这边使用自动生成证书 2. flutter build ios --release // 以创建release版本(flutt ...
- Android原生插件开发-开发篇
原创文档:Android原生插件开发-开发篇 · 语雀 官方文档:原生开发者支持 创建module 点击File=>New=>New Module 选择Android Library,输入 ...
最新文章
- LINUX下源码包安装mysql
- 人工操作阶段计算机是如何工作的,管理信息系统作业参考答案
- 【转】shell pipe与输入输出重定向的区别
- Tomcat JVM 初始化加大内存
- 低配本用win10服务器系统,低配电脑装win10最早版可以吗
- 《异度神剑2》与犹太教卡巴拉略考
- 项目管理工具——Jira使用和配置
- 什么是QCIF? CIF?2CIF?4CIF?DCIF?
- 100个免费可商用字体,你总有一天用到它
- 矩阵论——矩阵的标准型
- 80004005错误代码_win7系统出现错误代码0x80004005该如何解决
- 基因序列 深度学习Deep Learning for Genomics: A Concise Overview
- bmp/gif/jpg图象最底层原理分析
- MySQL子查询的优缺点_浅谈mysql的子查询
- k-选取问题之快速选取策略quickSelection
- 解决STM32新增加函数出现Undefined symbol HAL_ADC_Init (referred from main.o). 问题
- 智慧物业综合管理系统(java+vue+Uni-app),源码免费分享
- 网速测试大师的软件怎么回事,网速测试大师
- 使用libcurl库把域名转化IP
- Service启动泄露异常:android.app.ServiceConnectionLeaked
热门文章
- MySql中设置utf8编码方法
- 软件配置管理岗位职责说明
- Java 基于mail.jar 和 activation.jar 封装的邮件发送工具类
- Wps日期时间格式转文本、科学计数法转数字
- vscode之vue文件格式化代码无效怎么办
- cmd静默运行_如何在Win10上静默运行批处理文件
- 【LTspice】【使用.step命令对LTspice电路进行对比分析】
- 计算机怎样保存文件格式,word文档怎样保存为pdf格式
- iOS 14.5正式版如约而至 支持通过Apple Watch解锁iPhone
- 如何解压war后缀的文件: