Flutter:借助 jnigen通过原生互操作(Native Interop)使用 Android Intent。、

在这篇文章中,我将展示如何在 Dart/Flutter 中直接访问部分 Android API 。最终结果将是一个插件,允许向 Android 系统发送一些基本的 Intent(意图),例如打开电子邮件客户端或选择联系人。

欢迎关注我的公众号:OpenFlutter,谢谢

我们希望打开用户的默认电子邮件客户端来发送一封格式正确的邮件,但使用 package:url_launcher 创建 mailto: 的典型方法过于不可靠 。Android 允许提供一个特定类型为 ACTION_SENDTO 的 Intent,它将查询系统以找到处理该意图的最佳应用。我使用 jnigen 为这个 Intent 调用创建了绑定,以下是我的概述。

准备工作

创建一个新的 Flutter 插件,并修改 pubspec.yamljnigen.yaml 文件以匹配 jnigen 仓库中的示例。由于这只适用于 Android,因此我们运行以下命令:

bash 复制代码
flutter create --template plugin --platforms android android_intent
cd android_intent/example
flutter build apk --release

在清理代码和依赖项之后,请确保在发布模式下构建您的 Android 示例应用。您也可以在 Android Studio 中打开它,检查一切是否正确。

该设置假设以下软件包版本,并适用于 2025 年 7 月:

  • jnigen: ^0.14.0
  • jni: ^0.14.2

jnigen 设置

jnigen 插件的基本设置对我来说工作正常,尽管它会生成超过 2 万行的 Dart 代码。为了能够访问 Android API,您需要指定所有类 (及其依赖项,例如 android.net.Uri),这些类在 Java/Kotlin 代码中通常需要导入。以下 jnigen.yaml 文件对我有用:

YAML

vbnet 复制代码
android_sdk_config:
  add_gradle_deps: true
  android_example: "example/"
source_path:
  - "android/src/main/java"
classes:
  - "android.content.Intent"
  - "android.content.Context"
  - "android.app.Activity"
  - "android.net.Uri"
output:
  dart:
    path: "lib/src/bindings.dart"
    structure: "single_file"

一旦运行 jnigen 代码生成器,您就应该能够在 Dart 代码中直接使用生成的绑定了。

bash 复制代码
flutter pub run jnigen --config jnigen.yaml

在 Dart 中的用法

要发送一个简单的 Intent 来打开电子邮件客户端,您可以使用以下 Dart 代码:

dart 复制代码
void sendEmail() {
  try {
    final intent = Intent.new$2(Intent.ACTION_SENDTO);
    intent.setData(Uri.parse("mailto:".toJString()));

    intent.putExtra$21(
      Intent.EXTRA_EMAIL,
      JArray.of<JString>(JString.type, ["example@example.com".toJString()]),
    );
    intent.putExtra$8(Intent.EXTRA_SUBJECT, "Subject".toJString());
    intent.putExtra$8(Intent.EXTRA_TEXT, "Body text".toJString());
    intent.putExtra$21(
      Intent.EXTRA_CC,
      JArray.of<JString>(JString.type, ["cc@example.com".toJString()]),
    );
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    Context contextInstance = Context.fromReference(Jni.getCachedApplicationContext());
    contextInstance.startActivity(intent);
  } catch (e) {
    print('Error opening email: $e');
  }
}

非常酷的一点是,这段代码与 Android 文档 中的代码完全对应

kotlin 复制代码
fun composeEmail(addresses: Array<String>, subject: String) {
    val intent = Intent(Intent.ACTION_SENDTO).apply {
        data = Uri.parse("mailto:") // 只有电子邮件应用能处理这个。
        putExtra(Intent.EXTRA_EMAIL, addresses)
        putExtra(Intent.EXTRA_SUBJECT, subject)
    }
    if (intent.resolveActivity(packageManager) != null) {
        startActivity(intent)
    }
}

显然,存在一些略微烦人的差异:

  • 您必须将 Dart 类型转换为 Jni 类型,例如使用 toJString()
  • 您必须使用 JArray.of<JString>() 来创建一个字符串数组。
  • 您必须使用 putExtra$21putExtra$8 等方法,因为 Dart 不支持方法重载(method overloading)

另一方面,package:jni 带来了一些便利方法:

  • Jni.getCachedApplicationContext() 用于获取应用上下文,我可以将其转换为 Context 并用于启动 Intent。
  • 未在此处展示的 Jni.getCurrentActivity()

发送带结果的 Intent

直接通过 Activity.onActivityResult() 接收 Intent 结果是不可能的,所以我不得不寻找一个变通方案。主要的灵感来自 package:receive_intent,以及我之前尝试使用带有监听器接口的代理的方法。未来可能还有其他实现方式------详见 this issue

无论如何,要接收像这样的 Intent 结果,您需要覆盖主 Activity 的 onActivityResult 方法

Kotlin 复制代码
const val REQUEST_SELECT_CONTACT = 1
fun selectContact() {
    val intent = Intent(Intent.ACTION_PICK).apply {
        type = ContactsContract.Contacts.CONTENT_TYPE
    }
    if (intent.resolveActivity(packageManager) != null) {
        startActivityForResult(intent, REQUEST_SELECT_CONTACT)
    }
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    if (requestCode == REQUEST_SELECT_CONTACT && resultCode == RESULT_OK) {
        val contactUri: Uri = data.data
        // 对 contactUri 处的选中联系人执行操作。
    }
}

FlutterPlugin 附带了一个 ActivityAware 接口,允许您通过 ActivityPluginBinding 注册一个结果监听器:

Kotlin

kotlin 复制代码
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
    activity = WeakReference(binding.activity)
    binding.addActivityResultListener { res, req, intent ->
        Log.i("ARLP", "addActivityResultListener called with intent: $intent")
    }
}

因此,为了实现这一功能,我必须实现一个典型的 Flutter 插件类,它将持有 OnResultListener 接口。该接口稍后将在 Dart 中实现。

这也意味着我的插件现在包含了一个自己的 Kotlin 类,因此需要在 pubspec.yaml 文件中注册它:

YAML

yaml 复制代码
flutter:  
  plugin:
    platforms:
      android:
        ffiPlugin: true
+       package: com.example.android_intent
+       pluginClass: ActivityResultListenerProxy

接下来,我创建了一个 Kotlin 类 ActivityResultListenerProxy

Kotlin

kotlin 复制代码
@Keep
class ActivityResultListenerProxy : FlutterPlugin, ActivityAware {
    private var activity: WeakReference<Activity>? = null;
    private var callback: OnResultListener? = null;

    public interface OnResultListener {
        fun onResult(requestCode: Int, resultCode: Int, result: String?)
    }

    @Keep
    public fun onResult(requestCode: Int, resultCode: Int, intent: Intent?) : Boolean {
        if (intent != null) {
            callback?.onResult(requestCode, resultCode, intent.dataString)
            return true
        } else {
            return false
        }
    }

    override fun onAttachedToActivity(binding: ActivityPluginBinding) {
        activity = WeakReference(binding.activity)
        binding.addActivityResultListener { res, req, intent ->
            Log.i("ARLP", "onAttachedToActivity called with intent: $intent")
            onResult(res, req, intent)
        }
    }

    override fun onDetachedFromActivityForConfigChanges() {
        activity?.clear()
        activity = null
    }// other methods
}

一个挑战是如何获取由 Flutter 创建的插件实例。为了使其工作,我将其设计为单例(singleton),并添加了一个静态伴生对象(static companion object)来持有该实例。插件不能是 Kotlin 的 object

Kotlin

kotlin 复制代码
@Keep
class ActivityResultListenerProxy : FlutterPlugin, ActivityAware {
    private var callback: OnResultListener? = null;

    companion object {
        @Volatile
        private var instance: ActivityResultListenerProxy? = null

        fun getInstance(): ActivityResultListenerProxy {
            return instance ?: synchronized(this) {
                instance ?: ActivityResultListenerProxy().also { instance = it }
            }
        }
    }

    @Keep
    public fun setOnResultListener(listener: OnResultListener) {
        callback = listener
    }// other methods
}

然后,在更新我的 jnigen.yaml 之后,我就能够启动带结果的 Intent 了。

YAML

vbnet 复制代码
android_sdk_config:
  add_gradle_deps: true
  android_example: "example/"
source_path:
  - "android/src/main/java"
classes:
  - "android.content.Intent"
  - "android.content.Context"
  - "android.app.Activity"
  - "android.net.Uri"
  - "android.provider.ContactsContract"
  - "com.example.android_intent.ActivityResultListenerProxy"
output:
  dart:
    path: "lib/src/bindings.dart"
    structure: "single_file"

这次我不得不使用 Activity 来启动 Intent 并注册结果监听器。

dart 复制代码
void selectContact() {
  try {
    final intent = Intent.new$2(Intent.ACTION_PICK);
    intent.setType(ContactsContract$Contacts.CONTENT_TYPE);

    listener = ActivityResultListenerProxy.Companion.getInstance();
    listener!.setOnResultListener(
      ActivityResultListenerProxy$OnResultListener.implement(
        $ActivityResultListenerProxy$OnResultListener(
          onResult: (result, request, data) {
            print('ARLP Selected contact: $result, request: $request, data: $data');
          },
        ),
      ),
    );

    final activity = Activity.fromReference(Jni.getCurrentActivity());

    activity.startActivityForResult(intent, 1);
  } catch (e) {
    print('Error selecting contact: $e');
  }
}

我还尝试了更多其他的 Intent,例如从相机选择图片,它们似乎都是可行的。虽然还需要更多的 API 设计才能使其完全可用,但能够在 Dart 代码中直接使用 Android API 仍然非常酷。

您可以在我的 native_interop repository 中找到完整的源代码。

相关推荐
梨子同志12 小时前
qiankun
前端
梨子同志12 小时前
Nginx
前端
梨子同志12 小时前
NestJS
前端
梨子同志12 小时前
Next.js
前端
谭光志13 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
Hilaku13 小时前
前端架构师的克制:什么时候该坚决对新技术说不?
前端·javascript·程序员
陈随易13 小时前
前端项目部署只要30秒
前端·后端·程序员
本末倒置18313 小时前
从 Vue CLI 迁移到 Rsbuild 后,JSX 突然全挂了?一次排查记录
前端
古夕13 小时前
多应用同域部署下 Favicon 异常问题排查与治理
前端·javascript
YIAN13 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp