Flutter插件開發(fā)之HmsScanKit實現(xiàn)示例詳解
前沿
從事Flutter開發(fā)以來,一直都是使用已有的插件,沒有自己開發(fā)過。最近同事推薦讓我使用華為的掃碼SDK(hms_scan_kit),正好借此機會來開發(fā)一個Flutter的原生插件。算是對最近的插件學(xué)習(xí)做一個簡單的總結(jié)。
效果圖
我們先看一下實現(xiàn)的掃碼效果:點擊LoadScanKit按鈕調(diào)起插件的掃碼功能,掃碼成功后在界面顯示掃碼結(jié)果。
相關(guān)知識點
1. Flutter Packages
通過使用 package(的模式)可以創(chuàng)建易于共享的模塊化代碼。一個最基本的 package 由以下內(nèi)容構(gòu)成:
- pubspec.yaml 文件
用于定義 package 名稱、版本號、作者等其他信息的元數(shù)據(jù)文件。- lib 目錄
包含共享代碼的 lib 目錄,其中至少包含一個 <package-name>.dart 文件。
2. Package類別
Package包分為二種:
- 純Dart庫(Dart packages)
- 只用Dart編寫的傳統(tǒng)package,比如 path。
- 原生插件(Plugin packages)
- 使用Dart編寫的,按需使用Java或 Kotlin、Objective-C或Swift 分別在Android或iOS平臺實現(xiàn)的package。
3. 原生插件開發(fā)步驟
- 創(chuàng)建package
- 想要創(chuàng)建原生插件 package,請使用帶有 --template=plugin 標(biāo)志的 flutter create 命令
flutter create --org com.example --template=plugin --platforms=android,ios -a kotlin hello
實現(xiàn)package a. 定義 package API(.dart) b. 添加 Android/iOS 平臺代碼(.kt/.swift) C. 關(guān)聯(lián) API 和平臺代碼
指定插件支持的平臺,比如hms_scan插件就如下定義:
name: flutter_hms_scan description: A new Flutter project. version: 0.0.1 homepage: environment: sdk: ">=2.15.1 <3.0.0" flutter: ">=2.5.0" flutter: plugin: platforms: android: package: com.fitem.flutter_hms_scan pluginClass: HmsScanPlugin ios: pluginClass: HmsScanPlugin
備注:如果使用IDE(比如Android Studio)直接在創(chuàng)建Flutter項目處選擇Plugin類型即可,IDE會創(chuàng)建插件模板并實現(xiàn)獲取平臺系統(tǒng)版本的example,無需上面的步驟
- Dart對應(yīng)原生類型:
Dart | kotlin | Swift |
---|---|---|
null | null | nil |
bool | Boolean | NSNumber(value: Bool) |
int | Int | NSNumber(value: Int32) |
int | Long | NSNumber(value: Int) |
double | Double | NSNumber(value: Double) |
String | String | String |
Uint8List | ByteArray | FlutterStandardTypedData(bytes: Data) |
Int32List | IntArray | FlutterStandardTypedData(int32: Data) |
Int64List | LongArray | FlutterStandardTypedData(int64: Data) |
Float32List | FloatArray | FlutterStandardTypedData(float32: Data) |
Float64List | DoubleArray | FlutterStandardTypedData(float64: Data) |
List | List | Array |
Map | HashMap | Dictionary |
- Flutter的plugin通信流程如下:
HmsScan插件的實現(xiàn)
前面說了這么多,終于進入正題,下面我們開始HmsScan插件的開發(fā)吧。
1. 定義 package API:
class FlutterHmsScan { // 創(chuàng)建插件 static const MethodChannel _channel = MethodChannel('hms_scan'); // 定義調(diào)用方法 static Future<ScanBean> loadScanKit() async { return await _channel .invokeMethod("loadScanKit") .then((value) => scanBeanFromJson(json.encode(value))); } }
2. Android代碼實現(xiàn):
a. 使用IDE打開Android目錄,根據(jù)官方SDK導(dǎo)入庫
// scankitSDK implementation 'com.huawei.hms:scanplus:2.4.0.301' // 需要在repositories中導(dǎo)入url maven {url 'https://developer.huawei.com/repo/'}
b. 繼承FlutterPlugin類,接入Flutter管道。由于sdk用到權(quán)限請求和onActivityResult的回調(diào),因此我們需要繼承ActivityAware對Activity添加監(jiān)聽。其中registerWith()方法是為了適配老版本Flutter的兼容。
class HmsScanPlugin : FlutterPlugin, ActivityAware { /// The MethodChannel that will the communication between Flutter and native Android /// /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity private lateinit var mScanLauncher: ScanLauncher private lateinit var mHandler: MethodCallHandlerImpl /** * 老版本Flutter兼容 */ fun registerWith(registrar: Registrar) { mScanLauncher = ScanLauncher(registrar.context(), registrar.activity()) mHandler = MethodCallHandlerImpl(mScanLauncher) mHandler.startService(registrar.messenger()) registrar.addActivityResultListener(mHandler) registrar.addRequestPermissionsResultListener(mHandler) } override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { mScanLauncher = ScanLauncher(flutterPluginBinding.applicationContext, null) mHandler = MethodCallHandlerImpl(mScanLauncher) mHandler.startService(flutterPluginBinding.binaryMessenger) } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { mHandler.stopService() } override fun onAttachedToActivity(binding: ActivityPluginBinding) { mScanLauncher.activity = binding.activity binding.addActivityResultListener(mHandler) binding.addRequestPermissionsResultListener(mHandler) } override fun onDetachedFromActivity() { mScanLauncher.activity = null } override fun onDetachedFromActivityForConfigChanges() { onDetachedFromActivity() } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { onAttachedToActivity(binding) } }
c. 考慮到HmsScanPlugin職責(zé)過多,這里使用MethodCallHandlerImpl進行分離解耦,專門處理Flutter管道的通信。
/** * 插件方法監(jiān)聽 * Created by Fitem on 2022/3/2. */ class MethodCallHandlerImpl(var scanLauncher: ScanLauncher) : MethodChannel.MethodCallHandler, MethodCallHandlerListener, PluginRegistry.ActivityResultListener, PluginRegistry.RequestPermissionsResultListener { private lateinit var channel: MethodChannel override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "getPlatformVersion" -> { result.success("Android ${android.os.Build.VERSION.RELEASE}") } "loadScanKit" -> { scanLauncher.loadScanKit(call, result) } else -> { result.notImplemented() } } } override fun startService(binaryMessenger: BinaryMessenger) { channel = MethodChannel(binaryMessenger, "hms_scan") channel.setMethodCallHandler(this) } override fun stopService() { channel.setMethodCallHandler(null) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { if (resultCode != Activity.RESULT_OK || data == null) { return false } return scanLauncher.onActivityResult(requestCode, resultCode, data) } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>?, grantResults: IntArray? ): Boolean { if (permissions == null || grantResults == null) { return false } return scanLauncher.onRequestPermissionResult(requestCode, permissions, grantResults) } } // 管道通信生命周期的綁定 interface MethodCallHandlerListener { fun startService(binaryMessenger: BinaryMessenger) fun stopService() }
d. 最后通過ScanLauncher來專門處理掃碼功能的相關(guān)實現(xiàn)
class ScanLauncher(var applicationContext: Context, var activity: Activity?) { companion object { const val CAMERA_REQ_CODE = 111 const val DEFINED_CODE = 222 const val BITMAP_CODE = 333 const val MULTIPROCESSOR_SYN_CODE = 444 const val MULTIPROCESSOR_ASYN_CODE = 555 const val GENERATE_CODE = 666 const val DECODE = 1 const val GENERATE = 2 const val REQUEST_CODE_SCAN_ONE = 0X01 const val REQUEST_CODE_DEFINE = 0X0111 const val REQUEST_CODE_SCAN_MULTI = 0X011 const val DECODE_MODE = "decode_mode" const val RESULT = "SCAN_RESULT" const val SCAN_STATUS = "scanStatus" const val CODE_FORMAT = "codeFormat" const val RESULT_TYPE = "resultType" const val CODE_RESULT = "codeResult" } private var result: MethodChannel.Result? = null /** * 掃碼 */ fun loadScanKit(call: MethodCall, result: MethodChannel.Result) { this.result = result requestPermission(CAMERA_REQ_CODE, DECODE) } /** * Apply for permissions. */ private fun requestPermission(requestCode: Int, mode: Int) { if (activity == null) { result?.success(mapOf(SCAN_STATUS to false)) return } if (mode == DECODE) { decodePermission(requestCode) } else if (mode == GENERATE) { generatePermission(requestCode) } } /** * Apply for permissions. */ private fun decodePermission(requestCode: Int) { ActivityCompat.requestPermissions( activity!!, arrayOf(Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE), requestCode ) } /** * Apply for permissions. */ private fun generatePermission(requestCode: Int) { ActivityCompat.requestPermissions( activity!!, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), requestCode ) } fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Boolean { //Default View if (requestCode == REQUEST_CODE_SCAN_ONE) { val obj: HmsScan? = data.getParcelableExtra(ScanUtil.RESULT) if (obj != null) { result?.success( mapOf( SCAN_STATUS to true, CODE_FORMAT to getCodeFormat(obj.scanType), RESULT_TYPE to getResultType(obj), CODE_RESULT to obj.originalValue ) ) return true } //MultiProcessor & Bitmap } return false } fun onRequestPermissionResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ): Boolean { if (grantResults.size < 2 || grantResults[0] != PackageManager.PERMISSION_GRANTED || grantResults[1] != PackageManager.PERMISSION_GRANTED) { return false } //Default View Mode if (requestCode == CAMERA_REQ_CODE) { ScanUtil.startScan( activity, REQUEST_CODE_SCAN_ONE, HmsScanAnalyzerOptions.Creator().create() ) return true } return false } /** * 獲取CodeFormat */ private fun getCodeFormat(codeFormat: Int): String { return when (codeFormat) { HmsScan.QRCODE_SCAN_TYPE -> "QR code" HmsScan.AZTEC_SCAN_TYPE -> "AZTEC code" HmsScan.DATAMATRIX_SCAN_TYPE -> "DATAMATRIX code" HmsScan.PDF417_SCAN_TYPE -> "PDF417 code" HmsScan.CODE93_SCAN_TYPE -> "CODE93" HmsScan.CODE39_SCAN_TYPE -> "CODE39" HmsScan.CODE128_SCAN_TYPE -> "CODE128" HmsScan.EAN13_SCAN_TYPE -> "EAN13 code" HmsScan.EAN8_SCAN_TYPE -> "EAN8 code" HmsScan.ITF14_SCAN_TYPE -> "ITF14 code" HmsScan.UPCCODE_A_SCAN_TYPE -> "UPCCODE_A" HmsScan.UPCCODE_E_SCAN_TYPE -> "UPCCODE_E" HmsScan.CODABAR_SCAN_TYPE -> "CODABAR" else -> "OTHER" } } /** * 獲取ResultType */ private fun getResultType(hmsScan: HmsScan): String { return when (hmsScan.scanType) { HmsScan.QRCODE_SCAN_TYPE -> when (hmsScan.scanTypeForm) { HmsScan.QRCODE_SCAN_TYPE -> "Text" HmsScan.EVENT_INFO_FORM -> "Event" HmsScan.CONTACT_DETAIL_FORM -> "Contact" HmsScan.DRIVER_INFO_FORM -> "License" HmsScan.EMAIL_CONTENT_FORM -> "Email" HmsScan.LOCATION_COORDINATE_FORM -> "Location" HmsScan.TEL_PHONE_NUMBER_FORM -> "Tel" HmsScan.SMS_FORM -> "SMS" HmsScan.WIFI_CONNECT_INFO_FORM -> "Wi-Fi" HmsScan.URL_FORM -> "WebSite" HmsScan.URL_FORM -> "WebSite" else -> "Text" } HmsScan.EAN13_SCAN_TYPE -> when (hmsScan.scanTypeForm) { HmsScan.ISBN_NUMBER_FORM -> "ISBN" HmsScan.ARTICLE_NUMBER_FORM -> "Product" else -> "Text" } HmsScan.EAN8_SCAN_TYPE, HmsScan.UPCCODE_A_SCAN_TYPE, HmsScan.UPCCODE_E_SCAN_TYPE -> when (hmsScan.scanTypeForm) { HmsScan.ARTICLE_NUMBER_FORM -> "Product" else -> "Text" } else -> "Text" } } }
最后在AndroidManifest.xml中添加需要的權(quán)限:
<!--相機權(quán)限--> <uses-permission android:name="android.permission.CAMERA" /> <!--文件讀取權(quán)限--> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
3. ios部分的實現(xiàn)
ios原本也是打算使用hms的,但是官方居然2年沒有更新了,并且不支持bitcode版本、不支持cocopod,demo也無法正常運行。經(jīng)過一番嘗試后,決定放棄使用該庫,換成了MTBBarcodeScanner庫。(ios新人一個,如果有精通IOS的同學(xué)們歡迎指教?。?/p>
a. 通過SwiftHmsScanPlugin創(chuàng)建Flutter管道
public class SwiftHmsScanPlugin: NSObject, FlutterPlugin, BarcodeScannerViewControllerDelegate { private var result: FlutterResult? private var hostViewController: UIViewController? public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "hms_scan", binaryMessenger: registrar.messenger()) let instance = SwiftHmsScanPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { self.result = result if ("loadScanKit" == call.method) { loadScanKit() } else { result("iOS " + UIDevice.current.systemVersion) } } public func loadScanKit() { if let rootVC = UIApplication.shared.keyWindow?.rootViewController { hostViewController = topViewController(base:rootVC) } else if let window = UIApplication.shared.delegate?.window,let rootVC = window?.rootViewController { hostViewController = topViewController(base:rootVC) } let scannerViewController = BarcodeScannerViewController() let navigationController = UINavigationController(rootViewController: scannerViewController) if #available(iOS 13.0, *) { navigationController.modalPresentationStyle = .fullScreen } scannerViewController.delegate = self hostViewController?.present(navigationController, animated: false) } private func topViewController(base: UIViewController?) -> UIViewController? { if let nav = base as? UINavigationController { return topViewController(base: nav.visibleViewController) } else if let tab = base as? UITabBarController, let selected = tab.selectedViewController { return topViewController(base: selected) } else if let presented = base?.presentedViewController { return topViewController(base: presented) } return base } func didScanBarcodeWithResult(_ controller: BarcodeScannerViewController?, scanResult: ScanResult) { result?(["codeResult":scanResult.rawContent, "scanStatus" : String(true), "resultType": String(scanResult.format.rawValue)]) } func didFailWithErrorCode(_ controller: BarcodeScannerViewController?, errorCode: String) { result?(["scanStatus" : String(false)]) } }
b. BarcodeScannerViewController實現(xiàn)掃碼功能
class BarcodeScannerViewController: UIViewController { private var previewView: UIView? private var scanRect: ScannerOverlay? private var scanner: MTBBarcodeScanner? private let formatMap = [ BarcodeFormat.aztec : AVMetadataObject.ObjectType.aztec, BarcodeFormat.code39 : AVMetadataObject.ObjectType.code39, BarcodeFormat.code93 : AVMetadataObject.ObjectType.code93, BarcodeFormat.code128 : AVMetadataObject.ObjectType.code128, BarcodeFormat.dataMatrix : AVMetadataObject.ObjectType.dataMatrix, BarcodeFormat.ean8 : AVMetadataObject.ObjectType.ean8, BarcodeFormat.ean13 : AVMetadataObject.ObjectType.ean13, BarcodeFormat.interleaved2Of5 : AVMetadataObject.ObjectType.interleaved2of5, BarcodeFormat.pdf417 : AVMetadataObject.ObjectType.pdf417, BarcodeFormat.qr : AVMetadataObject.ObjectType.qr, BarcodeFormat.upce : AVMetadataObject.ObjectType.upce, ] var delegate: BarcodeScannerViewControllerDelegate? private var device: AVCaptureDevice? { return AVCaptureDevice.default(for: .video) } private var isFlashOn: Bool { return device != nil && (device?.flashMode == AVCaptureDevice.FlashMode.on || device?.torchMode == .on) } private var hasTorch: Bool { return device?.hasTorch ?? false } override func viewDidLoad() { super.viewDidLoad() UIDevice.current.endGeneratingDeviceOrientationNotifications() #if targetEnvironment(simulator) view.backgroundColor = .lightGray #endif previewView = UIView(frame: view.bounds) if let previewView = previewView { previewView.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(previewView) } setupScanRect(view.bounds) scanner = MTBBarcodeScanner(previewView: previewView) navigationItem.leftBarButtonItem = UIBarButtonItem(title: "cancel", style: .plain, target: self, action: #selector(cancel) ) updateToggleFlashButton() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if scanner!.isScanning() { scanner!.stopScanning() } UIDevice.current.endGeneratingDeviceOrientationNotifications() scanRect?.startAnimating() MTBBarcodeScanner.requestCameraPermission(success: { success in if success { self.startScan() } else { #if !targetEnvironment(simulator) self.errorResult(errorCode: "PERMISSION_NOT_GRANTED") #endif } }) } override func viewWillDisappear(_ animated: Bool) { scanner?.stopScanning() scanRect?.stopAnimating() UIDevice.current.beginGeneratingDeviceOrientationNotifications() if isFlashOn { setFlashState(false) } super.viewWillDisappear(animated) } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) setupScanRect(CGRect(origin: CGPoint(x: 0, y:0), size: size )) } private func setupScanRect(_ bounds: CGRect) { if scanRect != nil { scanRect?.stopAnimating() scanRect?.removeFromSuperview() } scanRect = ScannerOverlay(frame: bounds) if let scanRect = scanRect { scanRect.translatesAutoresizingMaskIntoConstraints = false scanRect.backgroundColor = UIColor.clear view.addSubview(scanRect) scanRect.startAnimating() } } private func startScan() { do { try scanner!.startScanning(with: cameraFromConfig, resultBlock: { codes in if let code = codes?.first { let codeType = self.formatMap.first(where: { $0.value == code.type }); let scanResult = ScanResult.with { $0.type = .barcode $0.rawContent = code.stringValue ?? "" $0.format = codeType?.key ?? .unknown $0.formatNote = codeType == nil ? code.type.rawValue : "" } self.scanner!.stopScanning() self.scanResult(scanResult) } }) } catch { self.scanResult(ScanResult.with { $0.type = .error $0.rawContent = "\(error)" $0.format = .unknown }) } } @objc private func cancel() { scanResult( ScanResult.with { $0.type = .cancelled $0.format = .unknown }); } @objc private func onToggleFlash() { setFlashState(!isFlashOn) } private func updateToggleFlashButton() { if !hasTorch { return } let buttonText = isFlashOn ? "flash_off" : "flash_on" navigationItem.rightBarButtonItem = UIBarButtonItem(title: buttonText, style: .plain, target: self, action: #selector(onToggleFlash) ) } private func setFlashState(_ on: Bool) { if let device = device { guard device.hasFlash && device.hasTorch else { return } do { try device.lockForConfiguration() } catch { return } device.flashMode = on ? .on : .off device.torchMode = on ? .on : .off device.unlockForConfiguration() updateToggleFlashButton() } } private func errorResult(errorCode: String){ delegate?.didFailWithErrorCode(self, errorCode: errorCode) dismiss(animated: false) } private func scanResult(_ scanResult: ScanResult){ self.delegate?.didScanBarcodeWithResult(self, scanResult: scanResult) dismiss(animated: false) } private var cameraFromConfig: MTBCamera { return .back } }
c. 最后需要在example的ios目錄Info.plist文件中添加相機權(quán)限:
// example/ios/Runner/Info.plist <key>NSCameraUsageDescription</key> <string>Camera permission is required for barcode scanning.</string>
至此,一個簡單的應(yīng)用于Android、iOS的plugin插件已完成。
4. 需要注意的點
- 使用Android Studio右鍵選擇Flutter即可通過Android Studio和Xcode打開項目,如圖:
- Android目錄打開后,若看不到插件module,可以選擇Project Files模式下查看,如圖:
- ios目錄打開前,需要進入example目錄輸入命令 flutter build ios,待編譯完成后再通過Xcode打開。
總結(jié)
Plugin原生插件其實就是基于Flutter提供的管道進行通信,和原生開發(fā)的使用并無太大區(qū)別。但需要我們對原生代碼的調(diào)用有一個基本的了解,然后引入其他原生開發(fā)庫進行調(diào)用。最后附上項目地址:flutter_hms_scan
以上就是Flutter插件開發(fā)之HmsScanKit實現(xiàn)示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Flutter插件開發(fā)HmsScanKit的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android?Jetpack庫剖析之LiveData組件篇
LiveData是Jetpack組件的一部分,更多的時候是搭配ViewModel來使用,相對于Observable,LiveData的最大優(yōu)勢是其具有生命感知的,換句話說,LiveData可以保證只有在組件( Activity、Fragment、Service)處于活動生命周期狀態(tài)的時候才會更新數(shù)據(jù)2022-07-07Android使用phonegap從相冊里面獲取照片(代碼分享)
本文主要介紹了使用phonegap從相冊里面獲取照片的實現(xiàn)方法代碼。具有很好的參考價值,下面跟著小編一起來看下吧2017-03-03Android網(wǎng)絡(luò)通信基礎(chǔ)類源碼分析講解
這篇文章主要介紹了Android網(wǎng)絡(luò)通信基礎(chǔ)類源碼,包括了Handler、Looper、Thread的分析講解,對日常開發(fā)學(xué)習(xí)很有幫助,需要的朋友可以參考下2024-05-05android 仿微信demo——微信消息界面實現(xiàn)(移動端)
本系列文章主要介紹了微信小程序-閱讀小程序?qū)嵗╠emo),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧,希望能給你們提供幫助2021-06-06Android?debug包運行正常release包崩潰的解決辦法
這篇文章主要介紹了Android?debug包運行正常,release包崩潰解決辦法,文中通過代碼示例介紹的非常詳細(xì),對大家解決問題有一定的幫助,需要的朋友可以參考下2024-04-04