Android進階學習:Flutter插件開發

跨端技術是Android程序員乃至所有移動開發程序員一直在研究的課題。
3月4日,谷歌正式發佈了 Flutter 的 2.0。該版本最大的特性就是可以支持五大主流的操作系統:iOS、Android、Linux、Windows 和 MacOS。官方甚至還說豐田將會把 Flutter 帶到汽車中。
也就是說,我們可以用一套 Flutter 代碼適配全平臺了。

推薦閱讀:Flutter 到底能不能成爲“跨平臺開發終極之選”?

今天要和大家分享的是掘金的一個大佬在Flutter插件開發上面的探索,希望本文對大家的學習和工作有所幫助。
作者:chonglingliu
鏈接:https://juejin.cn/post/6960851594816520228

本文只會涉及到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作爲一個模塊放到原生項目中進行混合開發。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章