跳转到正文
YGJoon 技术博客

Flutter 插件开发 Android 平台篇

发布于:
·1.2k 字 · 约 3 分钟·作者:
Kotlin 与 Java 两种写法实现 MethodChannel 插件,onAttachedToEngine 绑定、引擎解绑时清理,附权限申请处理
Flutter 插件开发 Android 平台篇文章封面
目录

开发环境配置

1. Android 开发环境

2. 项目配置

// android/build.gradle
android {
  compileSdkVersion 33
  
  defaultConfig {
    minSdkVersion 21
    targetSdkVersion 33
  }
}

dependencies {
  implementation 'androidx.core:core-ktx:1.9.0'
  // 其他依赖
}

插件实现

1. Kotlin 实现

// android/src/main/kotlin/com/example/example_plugin/ExamplePlugin.kt
class ExamplePlugin: FlutterPlugin, MethodCallHandler {
  private lateinit var channel: MethodChannel
  private lateinit var context: Context

  override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    context = binding.applicationContext
    channel = MethodChannel(binding.binaryMessenger, "example_plugin")
    channel.setMethodCallHandler(this)
  }

  override fun onMethodCall(call: MethodCall, result: Result) {
    when (call.method) {
      "getPlatformVersion" -> {
        result.success("Android ${android.os.Build.VERSION.RELEASE}")
      }
      else -> {
        result.notImplemented()
      }
    }
  }

  override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    channel.setMethodCallHandler(null)
  }
}

2. Java 实现

// android/src/main/java/com/example/example_plugin/ExamplePlugin.java
public class ExamplePlugin implements FlutterPlugin, MethodCallHandler {
  private MethodChannel channel;
  private Context context;

  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
    context = binding.getApplicationContext();
    channel = new MethodChannel(binding.getBinaryMessenger(), "example_plugin");
    channel.setMethodCallHandler(this);
  }

  @Override
  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
    if (call.method.equals("getPlatformVersion")) {
      result.success("Android " + android.os.Build.VERSION.RELEASE);
    } else {
      result.notImplemented();
    }
  }

  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
    channel.setMethodCallHandler(null);
  }
}

生命周期管理

1. Activity 感知

class ExamplePlugin: FlutterPlugin, ActivityAware {
  private var activity: Activity? = null

  override fun onAttachedToActivity(binding: ActivityPluginBinding) {
    activity = binding.activity
  }

  override fun onDetachedFromActivity() {
    activity = null
  }

  override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
    onAttachedToActivity(binding)
  }

  override fun onDetachedFromActivityForConfigChanges() {
    onDetachedFromActivity()
  }
}

2. 资源管理

class ExamplePlugin: FlutterPlugin {
  private var resources: Resources? = null
  private var disposables = CompositeDisposable()

  override fun onAttachedToEngine(binding: FlutterPluginBinding) {
    resources = binding.applicationContext.resources
  }

  override fun onDetachedFromEngine(binding: FlutterPluginBinding) {
    resources = null
    disposables.clear()
  }
}

权限处理

1. 权限声明

<!-- android/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.example.example_plugin">
  
  <uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

2. 运行时权限

class ExamplePlugin: FlutterPlugin, ActivityAware {
  private val PERMISSION_REQUEST_CODE = 100
  private var permissionResult: Result? = null

  private fun checkAndRequestPermission(permission: String, result: Result) {
    if (activity == null) {
      result.error("NO_ACTIVITY", "Plugin requires activity", null)
      return
    }

    if (ContextCompat.checkSelfPermission(activity!!, permission)
        == PackageManager.PERMISSION_GRANTED) {
      result.success(true)
    } else {
      permissionResult = result
      ActivityCompat.requestPermissions(
        activity!!,
        arrayOf(permission),
        PERMISSION_REQUEST_CODE
      )
    }
  }

  override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<String>,
    grantResults: IntArray
  ): Boolean {
    if (requestCode == PERMISSION_REQUEST_CODE && permissionResult != null) {
      permissionResult?.success(
        grantResults.isNotEmpty() &&
        grantResults[0] == PackageManager.PERMISSION_GRANTED
      )
      permissionResult = null
      return true
    }
    return false
  }
}

性能优化

1. 异步处理

class ExamplePlugin: FlutterPlugin {
  private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

  private fun performHeavyTask(result: Result) {
    scope.launch {
      try {
        // 执行耗时操作
        val data = processData()
        // 切换到主线程返回结果
        withContext(Dispatchers.Main) {
          result.success(data)
        }
      } catch (e: Exception) {
        withContext(Dispatchers.Main) {
          result.error("ERROR", e.message, null)
        }
      }
    }
  }

  override fun onDetachedFromEngine(binding: FlutterPluginBinding) {
    scope.cancel()
  }
}

2. 内存管理

class ExamplePlugin: FlutterPlugin {
  private var cache: LruCache<String, Bitmap>? = null

  override fun onAttachedToEngine(binding: FlutterPluginBinding) {
    val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
    val cacheSize = maxMemory / 8
    cache = object : LruCache<String, Bitmap>(cacheSize) {
      override fun sizeOf(key: String, bitmap: Bitmap): Int {
        return bitmap.byteCount / 1024
      }
    }
  }

  override fun onDetachedFromEngine(binding: FlutterPluginBinding) {
    cache?.evictAll()
    cache = null
  }
}

调试与测试

1. 日志记录

class ExamplePlugin: FlutterPlugin {
  companion object {
    private const val TAG = "ExamplePlugin"
  }

  private fun logDebug(message: String) {
    if (BuildConfig.DEBUG) {
      Log.d(TAG, message)
    }
  }

  private fun logError(message: String, error: Throwable? = null) {
    Log.e(TAG, message, error)
  }
}

2. 单元测试

// android/src/test/kotlin/com/example/example_plugin/ExamplePluginTest.kt
class ExamplePluginTest {
  @Test
  fun testPlugin() {
    val plugin = ExamplePlugin()
    // 测试代码
  }
}

最佳实践

1. 别抄 v1 embedding 的老教程

registerWith(Registrar) 在 Flutter 1.12 之后就废弃了,v1 embedding 的 shim 早已被移除。新插件一律用 FlutterPlugin + ActivityAware 结构;搜到带 registerWith 的教程直接关掉,抄来编译不过。

2. result 必须在主线程调用

onMethodCall 跑在主线程,但你的耗时操作在协程/线程池里——切记 result.success/result.error 要切回主线程再调,后台线程直接调会崩。上文协程示例里 withContext(Dispatchers.Main) 就是为这个,不是装饰。

3. 每个 MethodCall 必须 exactly once 回复

result.success、result.error、result.notImplemented 三选一,且只调一次。漏调,Dart 侧的 await 永远挂起;调两次,直接抛异常。用 try-finally 或协程的 invokeOnCompletion 兜底。

4. 盯住权限的版本差异

Android 6.0+ 要运行时申请(上文已讲),但更隐蔽的是 Android 12+:targetSdk 31+ 时蓝牙扫描/连接改用 BLUETOOTH_SCAN/BLUETOOTH_CONNECT,老教程里”申请位置权限连蓝牙”那套在新系统上直接拿不到设备。每个权限先查它是在哪个 API 级别引入的。

实战案例

flutter_aj_player 的 Android 端,说实话我没写多少原生代码——因为它底层用的是官方 video_player 插件,我的插件更多是 Dart 层的封装(加了 auto_orientation 全屏转屏、brightness_volume 手势调亮度音量)。真正的 Android 原生开发(Kotlin 写 MethodChannel)在我的项目里出场很少。唯一一次是调 Android 的 VideoView 行为,发现 video_player 的 ExoPlayer 封装已经够用了,就没往下挖。我的体会:先确认官方/社区插件能不能满足,别一上来就写原生。80% 的”要写插件”需求,其实是”没找对现有插件”。

总结

Android 平台插件开发的关键点:

  1. 合理的项目结构
  2. 完善的生命周期管理
  3. 规范的权限处理
  4. 优秀的性能表现

通过本文的学习,你应该已经掌握了 Flutter 插件在 Android 平台的开发要点。下一篇文章将介绍 iOS 平台插件的开发。


分享这篇文章:


相关文章

上一篇
Flutter 插件开发基础篇
下一篇
Flutter集成Sentry日志收集