移動端小白,30天掌握Flutter雙端外掛開發-中(Android篇)

樂凡丶發表於2021-05-25

前文提要:移動端小白,30天掌握Flutter雙端外掛開發-上(Flutter篇)

上回書說到Flutter外掛開發基本的2種資料通訊方式:及時觸發的MethodChannel和狀態監聽EventChannel,就可以自由的向原生端觸發方法,比如獲取版本號,申請許可權,或者與原生第三方包互動,並取得這些事件返回的資料在flutter層展示。

這個還是蠻容易掌握,跟著視訊教程大概花費了2天時間,基本的功能跑通了,也瞭解了一些原理。後面在實際開發的過程中繼續鞏固資料通訊的騷操作。

那麼問題來了,作為一個對AndroidiOS完全不懂的小白,原生開發需要用什麼語言?怎麼將sdk包匯入進去?資料型別如何對應?平常只能在聽大佬吹牛B的時候瞭解一些皮毛,對於實際上手也沒什麼用。一系列問題都毫無頭緒,該如何進入下一步?

image.png

不過有句老話說的好,當你學會百度(Google),你就掌握了一切知識。百度一下兩大移動端作業系統的區別,可以得知iOS必須用mac,必須要用xcode,必須要開發者賬號,必須瞭解開發者平臺,喬老爺子無時無刻不在告訴你,在我地盤這er你就得聽我的er,每一點都讓人蛋疼。而Android就沒那麼多門門道道,本身flutter開發就需要完善的Android環境,所以就可以直接開始了。

也只能先從Android開始,畢竟我是忠實的巨硬使用者。巨硬大法好!!!

一、安卓新寵kotlin

學習的第一步當然是瞭解當前平臺的開發語言,聽到Android應用程式開發時會想到哪種程式語言?當然是JAVA!

但是我們在搜尋7天入門安卓開發的同時,發現還有2017年穀歌正式開始支援的kotlin這麼語言,而且各大文章都寫明白了java和kotlin的優劣對比。綜合來看,java屬於屬於老牌開發語言,資源多,生態完善,效能略有優勢,但學習曲線還是比較陡峭;而kotlin作為新生代開發語言,受了到谷歌的大力推崇,相比較於前者,程式碼更加簡潔,新增了不少現代化的語法,而且學習曲線比較平滑,100%和現有java開發的包進行互動。

那麼根據我平常買手機配電腦的經驗,正所謂用新不用舊,那麼就決定是你了,KOTLIN!!!

Kotlin基礎語法使用,這篇文章介紹的已經夠用了,但只需要重點學習幾個語法。

1、資料型別

學習一門語言首先就是看資料型別,在dart中我們會使用nullboolintStringMap以及Uint8List這幾種型別,在原生中所對應的語言可在下表中檢視。

1020339-20201219092818049-1554076072.png (此圖建議儲存,常看常新)

不同的資料型別都有一些基本的增刪改查方法,有過其他程式語言基礎的,完全不需要去背,需要的時候去查就好,像查詞典一樣。

2、基礎語法

基礎語法在所有的變成語言中都大同小異,只需要注意不同語言的區別,kotlin和dart語法在很多地方都比較相似

定義變數

這裡和dart非常像,都擁有null safe機制。

  • var 被它修飾的變數屬性可讀可寫
  • val 被它修飾的變數屬性可讀,但是隻能被賦值一次(相當於java的final)
  • lateinit 主要用於延遲初始化,可以在生命週期內進行賦值
//宣告一般變數
val tag:String = "plugin"
//可以先宣告稍後對進行賦值
private lateinit var context: Context
//宣告一個可以為空的值
private var eventSink: EventChannel.EventSink? = null
複製程式碼

流程控制

流程控制無非就是條件判斷和迴圈,在這個外掛中最常用的就是ifforwhen三元運算

// 條件語句if
if (call.arguments != null) {
    println(call.arguments)
}
// 迴圈語句for
for (i in count) {
    println("$i")
}
// 條件執行語句 when 相當於其他語言的switch
when (call.method) {
  "setup" -> {
    setup(result)
  }
  else -> {
    print("null")
  }
}
// 三元運算
int a = 10
int b = a > 12 ? a : 12;
複製程式碼

函式&Lambda 表示式

// 如果無返回值,返回值型別可以省略
fun printSum(a: Int, b: Int): Int { 
    print(a + b)
    return a + b
}
// 使用Lambda表示式可以這樣寫,簡化很多程式碼
var printSum = (int a, int b) -> print(a + b) 
複製程式碼

class類

類在大部分語言中都有的,概念也都大同小異。在這個專案中全部的程式碼都在主class中進行編寫,所以我們只需要瞭解到class的繼承

比如flutter的外掛中,我們必須在主class繼承FlutterPlugin這個類,來擴充生命週期,及MethodCallHandler來擴充資料通訊方法。而使用這個類裡方法的時候,需要使用override宣告重新。

class DemoPlugin: FlutterPlugin, MethodCallHandler{
     // 屬於FlutterPlugin的方法
    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        context = binding.applicationContext
        channel = MethodChannel(binding.binaryMessenger, "hz_camera")
        channel.setMethodCallHandler(this)
    }
    // 屬於FlutterPlugin的方法
    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
    }
    
    // 屬於MethodCallHandler的方法
    override fun onMethodCall(call: MethodCall, result: Result) {
        when (call.method) {
          "setup" -> {
            setup(result)
          else -> {
            result.notImplemented()
          }
        }
    }
}
複製程式碼

在這些類中還有許多其他方法,可以直接檢視原始碼去看,再自己嘗試。

object物件表示式和物件宣告

根據官方解釋,Kotlin 用物件表示式物件宣告來實現建立一個對某個類做了輕微改動的類的物件,且不需要去宣告一個新的子類。

  • 物件表示式是在使用他們的地方立即執行。
  • 物件宣告是在第一次被訪問到時延遲初始化的。

我們最常用的就是物件表示式,但官方的解釋比較抽象。用我的理解,大白話就是:一個方法的引數,是包含多個物件的類,要對這個類進行override,通過物件表示式,可以自己去進行改動,達到自己所需要的效果。就像下面這樣:

window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
        // ...
    }
    override fun mouseEntered(e: MouseEvent) {
        // ...
    }
})
複製程式碼

學到這裡,kotlin算是入門了,還有非常多其他的方法及概念都還沒講到,其實我也還沒研(kan)究(kan),但基本上對於開發一個外掛所需要的基礎知識夠用了。上面的內容3分鐘就可以讀完,但要真正的使用上,我還是花費了3天時間去深入理解學習。

每瞭解一個知識點,就會發現背後還有更多的知識點。百度,CV,執行,報錯,如此往復,慢慢就掌握了。

喂,三分鐘啦!學學學學卵啊學!飲茶先啦!

image.png

二、功能實現

瞭解了kotlin的基本知識,就可以正式開始開發了,對於使用sdk,也可以抽象的理解成匯入外掛-執行外掛的方法。再細化一點,就是引入sdk -> 匯入到檔案中 -> 執行方法 -> 獲得資料返回給flutter層。對開發流程拆解後,就可以從這幾個方面著手,來一步一步實現。

1、引入aar(SDK)

在我的flutter 引入第三方aar實踐這篇文章中,已經講了如何引入aar,不過這是直接在flutter主專案目錄中使用,和原專案繫結較深,不符合flutter外掛化的開發思想。而在外掛中引入,基本一致,但有一些小小的注意的地方。

直接執行外掛的expamle

這裡和那篇文章一樣。

  • 1、右鍵專案目錄,滑鼠放在Flutter選項上,再點選子選項open android module with android studio
  • 2、在android目錄下新建libs空資料夾
  • 3、將需要用到包複製進libs
  • 4、在android/setting.gradle中加入一行include ':libs:[aar name]'(包名不要字尾)
  • 5、在android/bulid.gradle的修改
dependencies {
    ...
    implementation project(':[aar name]')
}
複製程式碼
  • 6、最後一步就是點選gradle async(必須點選open android module後才有這個按鈕)

原生專案引入此外掛執行

上面的方法執行外掛demo沒什麼問題,但用在正式專案中就會發現,無法識別這個外掛,所以在原生專案中使用的話,需要改動一點。

這裡比較簡單,在將aar放進libs後,直接在在android/bulid.gradle加入一行即可,並先註釋到上面的語句。

dependencies {
    ...
    // implementation project(':[aar name]')
    implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
}
複製程式碼

建議在demo中將所有功能都實現後,在把引包的方式改為正式使用的方法。

2、匯入到檔案中

我們使用的sdk包是一個全景相機,主要功能有初始化、連線、監聽相機連線狀態、預覽、拍照等功能。上一步完成後,在main的主kt檔案中使用import就可以匯入包。通過雙擊aar檔案,可以檢視包的內部有哪些內容,並得知包裡檔案的路徑。

我們使用的小紅屋8k相機的aar,包名為HZcameraSDK,根據文件和包的內容,可以瞭解主要使用的方法。

  • HZCameraEnv主要用於初始化
  • HZCameraConnector用於連線相機,監聽相機連線狀態
  • HZCameraManager用於拍照,獲取拍照的圖片
  • HZCameraPreviewer用於實時預覽

其中監聽相機連線狀態,實時預覽的資料流,需要在初始化時進行監聽,宣告週期結束後取消監聽。MethodChannelEventChannel同樣也需要初始化,所以程式碼如下:

// HzCameraPlugin.kt
···
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.MethodChannel.MethodCallHandler

import com.hozo.camera.library.cameramanager.*
import com.hozo.camera.library.previewer.HZCameraPreviewer

class HzCameraPlugin: FlutterPlugin, MethodCallHandler, ActivityAware{
  private lateinit var channel : MethodChannel
  private lateinit var activity: Activity
  private lateinit var mPreviewer: HZCameraPreviewer
  private lateinit var context: Context
  
  // 宣告週期的初始化方法
  override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    context = binding.applicationContext
    // 繫結MethodChannel
    channel = MethodChannel(binding.binaryMessenger, "hz_camera")
    channel.setMethodCallHandler(this)

    // 繫結EventChannel
    val eventChannel = EventChannel(binding.binaryMessenger, "HzCamera_event")
    eventChannel.setStreamHandler(
      object : EventChannel.StreamHandler {
        override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
          eventSink = events
        }
        override fun onCancel(arguments: Any?) {
          eventSink = null
        }
      }
    )
    // 監聽相機狀態
    HZCameraConnector.sharedConnector().setCallback(object : HZCameraConnector.ICallback {
      override fun onCameraConnected() {
        activity.runOnUiThread{
          eventSink?.success(hashMapOf(
            "code" to 0,
            "data" to "連線成功"
          ))
        }
        switchToCamera()
      }

      override fun onCameraConnectFailed(errType: HZCameraConnector.ErrorType) {
        activity.runOnUiThread{
          eventSink?.success(hashMapOf(
            "code" to 0,
            "data" to errType.name
          ))
        }
      }

      override fun onCameraDisconnected(errType: HZCameraConnector.ErrorType) {
        Log.d(_tag, errType.name)
        activity.runOnUiThread{
          eventSink?.success(hashMapOf(
            "code" to 0,
            "data" to errType.name
          ))
        }
      }
    })

    // 初始化預覽
    mPreviewer = HZCameraPreviewer(context) { _, _ , _, _ -> }
    // 監聽預覽回撥資料
    mPreviewer.setCalibratedFrameCallback { frameData, width, height ->
      activity.runOnUiThread{
        eventSink?.success(hashMapOf(
          "code" to 1,
          "data" to hashMapOf(
            "frameData" to frameData,
            "width" to width,
            "height" to height,
          )
        ))
      }
    }

  }
  // 解除安裝掉channel
  override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    channel.setMethodCallHandler(null)
  }
  // 初始化activity
  override fun onAttachedToActivity(binding: ActivityPluginBinding) {
    activity = binding.activity
  }
  override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
    activity = binding.activity
  }
  override fun onDetachedFromActivityForConfigChanges() {
  }
  override fun onDetachedFromActivity() {
  }
  ...
}

複製程式碼

這裡主要搞明白的有以下幾點。

  • onAttachedToEngineonDetachedFromEngine是FlutterPlugin提供的,一般的註冊,解除安裝方法都在這裡執行,而我們的相機狀態監聽,實時相機畫面資料流監聽,繫結2種Channel等都在這裡執行。
  • 這裡的activity註冊以前可以使用PluginRegistry,但現在使用的flutterEmbedding版本為2,舊方法被廢棄了,需要使用新的方式註冊,就是ActivityAware,而這個類提供的onAttachedToActivity方法進行繫結。

3、執行方法

在上面初始化中,我們已經執行了相機狀態監聽和預覽回撥方法。你要問為什麼這樣寫,我只能說CV大法好,在學習一門新的語言,最快的方法是看別人的示例,然後跑起來看效果。在寫這些方法之前,在pub和github看了非常多其他人開發的第三方外掛,明白了這樣寫能執行。然後檢視這個包裡的所提供的方法進行替換就好。

這裡就輕車熟路了:

// 1. SDK初始化
  private fun setup(result: Result) {
    checkPermission()
    HZCameraEnv.setup(activity.application)
  }
  // 2. 連線相機
  private fun connectCamera() {
    val wifiManager = activity.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
    val wifiInfo: WifiInfo = wifiManager.connectionInfo
    wifiName = wifiInfo.ssid.replace("\"", "")
    HZCameraConnector.sharedConnector().connectCamera(wifiName)
  }
  //3. 開始預覽
  private fun startPreview() {
      mPreviewer.startPreview()
  }
  //4. 結束預覽預覽
  private fun startPreview() {
      mPreviewer.startPreview()
  }
  // 5. 拍攝照片
  private fun takePhoto() {
    // 未連線相機的時候無法拍照
    if (!HZCameraConnector.sharedConnector().isConnected) return
    HZCameraManager.sharedManager().takePhoto(
      // 設定延遲
      HZCameraStateModel.HZTakePhotoDelayInterval.kDelaySec3,
      // 狀態
      object : HZCameraManager.HZITakePhotoProgressDelegate{
        override fun onFailed(event: HZCameraEvent?, errorCode: Int) {
          ...
        }
        //拍照開始
        override fun onTakePhotoStart() {
          ...
        }
        // 拍照完成
        override fun onCapture(photoResName: String, photoFileIndex: Int, isSaved: Boolean) {
          ...
        }
        //拍照結束
        override fun onTakePhotoEnd() {
            ...
        }

      }
    )
  }
  // 6.獲取相機引數
  private fun getSystemInfo(result: Result) {
    HZCameraSettings.sharedSettings().getSystemInfo(
      object : HZCameraSettings.HZIReadSystemInfoCallback{
        override fun onSystemInfoReceived(systemInfo: HZSystemInfoModel) {
          activity.runOnUiThread {
            result.success(hashMapOf(
              "mBatteryPercent" to systemInfo.mBatteryPercent,
              "mChargingState" to systemInfo.mChargingState.name,
              "freeMemorySpaceWithUnitG" to systemInfo.freeMemorySpaceWithUnitG.toString(),
            ))
          }
        }
        override fun onSucceed(p0: HZCameraEvent?) {
        }
        override fun onFailed(p0: HZCameraEvent?, p1: Int) {
        }
      }
    )
  }

複製程式碼

4、獲取資料並返回flutter層

以上我們已經成功呼叫了sdk所提供的方法,接下來就是將這些資料返回給flutter進行展示。在掌握Flutter雙端外掛開發-上中,我們已經掌握了資料通訊,但上面第2步初始化方法中也寫的資料通訊,多了一些知識點。

一般的及時返回資料可以直接使用result.success()來返回資料

  override fun onMethodCall(call: MethodCall, result: Result) {
    when (call.method) {
      "getSystemInfo" -> {
        getSystemInfo(result)
      }
      else -> {
        result.notImplemented()
      }
    }
  }
  // 檢視第3步裡第6個方法,展示瞭如何返回hashMap型別
  private fun getSystemInfo(result: Result) {
      ...
  }
複製程式碼

通過onMethodCall裡resutlt所提供的方法,進行返回資料,可以直接返回boolintStringhashMap等任意型別。

需要主動推送的資料通過eventSink來通知flutter層

mPreviewer.setCalibratedFrameCallback { frameData, width, height ->
  activity.runOnUiThread{
    eventSink?.success(hashMapOf(
      "code" to 1,
      "data" to hashMapOf(
        "frameData" to frameData,
        "width" to width,
        "height" to height,
      )
    ))
  }
}
複製程式碼

比如在預覽資料流中,需要不斷的像flutter層推送資料流,則使用註冊好的eventSink?.success(),同意可以返回任意資料型別。

runOnUiThread 是什麼?

一般來講,直接使用result.success()eventSink.success()就可以返回資料,在demo中和一般的方法中也沒什麼問題,但是在object物件表示式中,會發現報錯,程式直接崩潰。

Methods marked with @UiThread must be executed on the main thread

對於我這個小白來講這個問題比較抽象。通過百度得知,出現該異常的主要原因是Flutter1.7.8版本新增了執行緒安全,需要原生在主執行緒中返回給Flutter。在Android中,執行緒有更多一些的細分名目:主執行緒、子執行緒、HandlerThread、IntentService、AsyncTask。

主執行緒的響應速度不應該受到影響,所以所有耗時操作都應該放置到子執行緒中進行。

而一般情況下的方法都是執行在主執行緒中,故直接返回也沒有問題。而像獲取相機電量,拍照,預覽都屬於耗時操作,都在子執行緒中。瞭解這麼多,也就夠用了。

這問題也就比較好解決了,就是在物件表示式裡需要返回資料的時候,包裝一層activity.runOnUiThread(),使用主執行緒來返回資料,就沒問題了。

三、總結

完成上面的功能,一共花費了我15個工作日左右。對於我而言,最難的地方其實是如何把包引入進來,3行程式碼花費了我3天,第一步果然是最艱難的。瞭解kotlin語言花費了一天,粗看一遍,然後就是跑一下程式碼示例,能執行就行。最簡單的其實是寫包裡要用的方法了,一邊看這大佬的示例,CV過來然後把函式名替換成自己包裡的方法就開始除錯。還有就是講預覽資料流轉為能看的花費了不少時間,在RGB位元組資料流轉BMP圖片格式這篇也寫明白了。

這個只是一個全景相機的sdk,一共就用了上面那些知識點,雖然對於安卓開發這點知識只是冰山一角,還有很長的路要走,但開發一個原生第三方android外掛勉強夠用了。對於其他的sdk,思路應該也就是這些吧,應該可以有些啟發。

← To Be Continued 。。。

相關文章