Creating an iOS Flutter Audio Player Plugin with MethodChannel
This tutorial explains step‑by‑step how to create a custom iOS Flutter plugin for audio playback, covering the MethodChannel architecture, Dart side implementation, Swift native code, and communication of playback controls and status between Flutter and iOS.
Flutter’s vision is that a developer can write a single codebase and run it on multiple platforms. Although many third‑party plugins exist, custom features such as a proprietary Bluetooth protocol often require a native plugin. This article shows how to implement a Flutter plugin that controls audio playback on iOS.
Prerequisites
The iOS project must be opened with Xcode. The example builds a music player with functions for play, pause, resume, seek, and duration retrieval, all exposed through a native plugin.
Architecture Overview
Flutter and native code communicate via a MethodChannel . The Dart side sends method calls, and the iOS side receives them, performs the native operation, and invokes callbacks to report progress, duration, completion, or errors.
Flutter Side – Dart Implementation
A singleton AudioPlayer class defines a MethodChannel named "netmusic.com/audio_player" . It provides methods that invoke native calls ( play , pause , resume , seek ) and registers a handler to receive callbacks from iOS.
class AudioPlayer {
// singleton
factory AudioPlayer() => _getInstance();
static AudioPlayer _instance;
AudioPlayer._internal() {
channel.setMethodCallHandler(nativePlatformCallHandler);
}
static const channel = MethodChannel("netmusic.com/audio_player");
// ... other members and methods (play, pause, resume, seek, etc.)
Future
nativePlatformCallHandler(MethodCall call) async {
final args = call.arguments as Map
;
switch (call.method) {
case "onPosition":
// update current position
break;
case "onDuration":
// update total duration
break;
case "onComplete":
// handle completion
break;
case "onError":
// handle error
break;
}
}
}iOS Side – Swift Implementation
The AppDelegate creates a PlayerWrapper instance and passes the FlutterViewController to it. PlayerWrapper sets up a FlutterMethodChannel with the same name and registers a handler for incoming method calls.
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 = window?.rootViewController as! FlutterViewController
playerWrapper = PlayerWrapper(vc: controller)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
} import Foundation
import Flutter
import AVKit
import CoreMedia
class PlayerWrapper: NSObject {
var vc: FlutterViewController
var channel: FlutterMethodChannel
var player: AVPlayer?
init(vc: FlutterViewController) {
self.vc = vc
channel = FlutterMethodChannel(name: "netmusic.com/audio_player", binaryMessenger: vc.binaryMessenger)
super.init()
channel.setMethodCallHandler(handleFlutterMessage)
> }
func handleFlutterMessage(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let method = call.method
let args = call.arguments as? [String: Any]
> if method == "play" {
guard let urlString = args?["url"] as? String, let url = URL(string: urlString) else { result(0); return }
> player?.pause()
> let asset = AVAsset(url: url)
> let item = AVPlayerItem(asset: asset)
> player = AVPlayer(playerItem: item)
> player?.play()
> // periodic time observer to report position
> player?.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 1), queue: nil) { [weak self] time in
> self?.channel.invokeMethod("onPosition", arguments: ["value": time.value / Int64(time.timescale)])
> }
> // observe status for duration and errors
> item.observe(\AVPlayerItem.status) { [weak self] playerItem, _ in
> if playerItem.status == .readyToPlay {
> if let duration = self?.player?.currentItem?.asset.duration {
> self?.channel.invokeMethod("onDuration", arguments: ["value": duration.value / Int64(duration.timescale)])
> }
> } else if playerItem.status == .failed {
> self?.channel.invokeMethod("onError", arguments: ["value": "play failed"])
> }
> }
> // completion notification
> NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: item, queue: nil) { [weak self] _ 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 { result(0); return }
> let seekTime = CMTimeMake(value: Int64(position), timescale: 1)
> player?.seek(to: seekTime)
> }
> }
>}Flutter Receiving Callbacks from iOS
The iOS side invokes onPosition , onDuration , onComplete , and onError . The Dart nativePlatformCallHandler updates streams ( onCurrentTimeChanged , onTotalTimeChanged , etc.) which can be consumed by StreamBuilder widgets to reflect playback state in the UI.
StreamBuilder(
stream: AudioPlayer().onTotalTimeChanged,
builder: (context, snapshot) {
if (!snapshot.hasData) return Text("00:00");
return Text(AudioPlayer().totalPlayTimeStr);
},
)With the above pieces, a functional audio player plugin is assembled. The article concludes by linking to the full source code and mentions that a follow‑up will cover the Android counterpart.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.