跨端技術是Android程序員乃至所有移動開發程序員一直在研究的課題。
3月4日,谷歌正式發佈了 Flutter 的 2.0。該版本最大的特性就是可以支持五大主流的操作系統:iOS、Android、Linux、Windows 和 MacOS。官方甚至還說豐田將會把 Flutter 帶到汽車中。
也就是說,我們可以用一套 Flutter 代碼適配全平臺了。
推薦閱讀:Flutter 到底能不能成爲“跨平臺開發終極之選”?
今天要和大家分享的是掘金的一個大佬在Flutter插件開發上面的探索,希望本文對大家的學習和工作有所幫助。
作者:chonglingliu
鏈接:https://juejin.cn/post/6960851594816520228
本文只會涉及到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作爲一個模塊放到原生項目中進行混合開發。
個人感覺這個方案目前應該是一個比較穩妥的方案,接下來我將會介紹這方面的內容。