likes
comments
collection
share

Flutter插件开发---iOS篇

作者站长头像
站长
· 阅读数 7

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

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

前言

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

Flutter插件开发---iOS篇

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

说明:

  1. 由于是音频播放,我制作GIF的时候没法体现音乐元素,所以音乐只能我自己独自欣赏了,哈哈~~
  2. 本文先只介绍iOS的插件制作,下篇文章我们再来介绍Android的插件制作。

架构概览

Flutter插件开发---iOS篇

我们从上面的官方架构图可以看出,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: 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)
    }
}

  1. FlutterAppDelegate的根视图就是一个FlutterViewController,这个我们在以前的文章中有介绍;
  2. 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()
    }
}
  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.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);
    }
}
  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 {
  // 定义一个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<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! 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的插件编写。