Flutter外掛開發---Android篇

chonglingliu發表於2021-05-11

上一篇文章我們一起來實現了iOS平臺的外掛開發,本節我們來看看Android平臺的外掛是如何實現的。

本文只會涉及到Android端的程式碼了,因為Flutter端程式碼是通用的,不需要修改了。

網路設定相關的修改

GoogleAndroid 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作為一個模組放到原生專案中進行混合開發。

個人感覺這個方案目前應該是一個比較穩妥的方案,接下來我將會介紹這方面的內容。

相關文章