上一篇文章我們一起來實現了iOS平臺的外掛開發,本節我們來看看Android平臺的外掛是如何實現的。
本文只會涉及到Android端的程式碼了,因為Flutter端程式碼是通用的,不需要修改了。
網路設定相關的修改
Google從Android P開始要求使用加密連線,如果應用使用的是非加密的明文流量的http
網路請求,則會導致該應用無法進行網路請求。
本專案中的圖片等有使用到http
網路請求,需要適配下:
- 在
res
新建一個xml
目錄; - 在
xml
目錄中新建一個network_permission_config.xml
檔案,內容如下:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
複製程式碼
AndroidManifest.xml
中新增配置
<application
android:label="netmusic_flutter"
android:icon="@mipmap/ic_launcher"
// 新增的設定
android:networkSecurityConfig="@xml/network_permission_config"
>
</application>
複製程式碼
Flutter端向Android端傳送訊息
Flutter端的程式碼
省略,程式碼同上篇文章。
Android端的程式碼
- 新建播放器控制類
PlayerWrapper
class PlayerWrapper(engine: FlutterEngine, val context: Context) {
}
複製程式碼
建構函式傳入了
FlutterEngine
: 因為FlutterEngine
中包含BinaryMessenger
,被用於建立MethodChannel
。
MainActivity
中初始化PlayerWrapper
class MainActivity: FlutterActivity() {
private var playerWrapper: PlayerWrapper? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
// 初始化播放器
playerWrapper = PlayerWrapper(flutterEngine, this)
super.configureFlutterEngine(flutterEngine)
}
}
複製程式碼
從
configureFlutterEngine()
函式中獲取到FlutterEngine
,然後建立PlayerWrapper
- 播放器控制類
PlayerWrapper
中建立MethodChannel
,然後註冊回撥函式
class PlayerWrapper(engine: FlutterEngine, private val context: Context) {
// 1. 新建MethodChannel
private var channel: MethodChannel = MethodChannel(engine.dartExecutor.binaryMessenger, "netmusic.com/audio_player").also {
// 2. 註冊回撥函式 handleMethodCall
it.setMethodCallHandler { call, result ->
try {
handleMethodCall(call, result)
result.success(1)
} catch (e: Exception) {
result.success(0)
}
}
}
}
複製程式碼
我們給
MethodChannel
註冊了一個匿名函式,當Flutter呼叫原生程式碼時候能夠收到對應的method
(方法名)和argument
(引數)。真正的處理方法在handleMethodCall
中。
handleMethodCall
中處理邏輯
private fun handleMethodCall(call: MethodCall, response: MethodChannel.Result) {
when (call.method) {
"play" -> {
// 1.1. 進行引數判斷
// 1.2. 建立播放器然後進行播放
// 1.3. 註冊音樂播放完成的回撥函式, 播放完成後傳送給Flutter
// 1.4. 註冊音樂播放失敗的回撥函式,播放失敗後傳送給Flutter
// 1.5. 開始一個定時器獲取當前的播放進度,把進度傳送給Flutter
}
"resume" -> {
// 2.1. 開始播放
// 2.2. 開啟定時器任務
}
"pause" -> {
// 3.1. 暫停播放
// 3.2. 取消定時器任務
}
"stop" -> {
// 4.1. 停止播放
// 4.2. 取消定時器任務
}
"seek" -> {
// 5.1. 判斷位置引數
// 5.2. 跳轉到某個地方進行播放
}
}
}
複製程式碼
我這裡只寫了邏輯,沒寫程式碼。接下來貼一下程式碼一對比就很清晰了。
Android端向Flutter端傳送訊息
Android端向Flutter端傳送訊息通過channel.invokeMethod
方法實現。
整個Android外掛的全部程式碼如下:
MainActivity.kt
class MainActivity: FlutterActivity() {
private var playerWrapper: PlayerWrapper? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
// 初始化播放器
playerWrapper = PlayerWrapper(flutterEngine, this)
super.configureFlutterEngine(flutterEngine)
}
}
複製程式碼
PlayerWrapper.kt
class PlayerWrapper(engine: FlutterEngine, private val context: Context) {
// 播放器
private var player: MediaPlayer? = null
// 當前的播放時間的定時器
private val positionTimer: Timer = Timer()
private var timerTask: PositionTimerTask? = null
// handler
private val handler: Handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
when (msg?.what) {
1 -> {
val obj = (msg.obj as Int) / 1000
// 1.5. 開始一個定時器獲取當前的播放進度,把進度傳送給Flutter
channel.invokeMethod("onPosition", mapOf("value" to obj))
}
}
}
}
// MethodChannel
private var channel: MethodChannel = MethodChannel(engine.dartExecutor.binaryMessenger, "netmusic.com/audio_player").also {
it.setMethodCallHandler { call, result ->
try {
handleMethodCall(call, result)
result.success(1)
} catch (e: Exception) {
result.success(0)
}
}
}
private fun handleMethodCall(call: MethodCall, response: MethodChannel.Result) {
when (call.method) {
"play" -> {
// 1.1. 進行引數判斷
val url = call.argument<String>("url") ?: throw error("播放地址錯誤")
player?.stop()
player?.release()
// 1.2. 建立播放器然後進行播放
player = MediaPlayer().also { player ->
player.setOnPreparedListener {
print("setOnPreparedListener")
// 回撥音樂的時長
channel.invokeMethod("onDuration", mapOf("value" to player.duration / 1000))
player.start()
}
// 1.3. 註冊音樂播放完成的回撥函式, 播放完成後傳送給Flutter
player.setOnCompletionListener {
// 回撥音樂播放完成
channel.invokeMethod("onComplete", mapOf<String, Any>())
}
player.setOnSeekCompleteListener {
player.start()
}
// 1.4. 註冊音樂播放失敗的回撥函式,播放失敗後傳送給Flutter
player.setOnErrorListener { mp, what, extra ->
print("$mp $what $extra")
// 回撥音樂播放失敗
channel.invokeMethod("onError", mapOf("value" to "play failed"))
true
}
player.setDataSource(this.context, Uri.parse(url))
player.prepareAsync()
}
// 1.5. 開始一個定時器獲取當前的播放進度,把進度傳送給Flutter
timerTask?.cancel()
timerTask = PositionTimerTask()
positionTimer.schedule(timerTask, 1000, 1000)
}
"resume" -> {
// 2.1. 開始播放
player?.start()
// 2.2. 開啟定時器任務
timerTask?.cancel()
timerTask = PositionTimerTask()
positionTimer.schedule(timerTask, 1000, 1000)
}
"pause" -> {
// 3.1. 暫停播放
player?.pause()
// 3.2. 開啟定時器任務
timerTask?.cancel()
timerTask = PositionTimerTask()
positionTimer.schedule(timerTask, 1000, 1000)
}
"stop" -> {
// 4.1. 停止播放
player?.stop()
// 4.2. 取消定時器任務
timerTask?.cancel()
}
"seek" -> {
// 5.1. 判斷位置引數
val position = call.argument<Int>("position") ?: throw error("拖動播放出現錯誤")
// 5.2. 跳轉到某個地方進行播放
player?.seekTo(position)
}
}
}
// 定時任務呼叫Handler傳送Message到主執行緒
inner class PositionTimerTask: TimerTask() {
override fun run() {
if (player?.isPlaying == true) {
val message = Message().also {
it.what = 1
it.obj = player?.currentPosition ?: 0
}
handler.sendMessage(message)
}
}
}
}
複製程式碼
特別說明:這裡使用Handler是因為
channel.invokeMethod
需要在主執行緒中呼叫。
總結
Google的理想是其他平臺為Flutter專案提供外掛實現跨平臺的開發。但是由於各種原因,目前的很多的應用場景可能只是將Flutter作為一個模組放到原生專案中進行混合開發。
個人感覺這個方案目前應該是一個比較穩妥的方案,接下來我將會介紹這方面的內容。