移动端小白,30天掌握Flutter双端插件开发-中(Android篇)

2,268 阅读13分钟

前文提要:移动端小白,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 。。。