文章

Intent 与组件通信深度解析——跨平台开发者的 Android 原生交互指南

面向跨平台开发者的 Android Intent 完全指南,从显式/隐式 Intent 原理到打开系统相册、分享、Deep Link 跳转等 Native 功能调用,系统梳理 Flutter/RN/鸿蒙开发中与 Android 组件通信的必备知识。

Intent 与组件通信深度解析——跨平台开发者的 Android 原生交互指南

一句话概括

Android Intent 是应用组件之间、应用与应用之间的消息传递机制——它就像 Android 世界的”快递系统”,无论是启动页面、打开相机、分享内容还是接收 Deep Link 跳转,背后都是 Intent 在完成消息的封装、匹配和传递。

背景与意义

如果你是 Flutter 或 React Native 开发者,你一定会遇到这样的需求:打开系统相册让用户选择图片、调用系统分享功能分享一段文本、或者从第三方应用跳转到你的应用某个具体页面。这些功能在 Android 平台上都是通过 Intent 完成的。

在 Flutter 中,你可能会用 image_picker 插件打开相机,在 React Native 中会用 react-native-image-pickerShare API。这些第三方插件的背后,本质上都是在调用 Android 原生的 Intent 机制。

但当你遇到”插件不支持的功能”、”需要自定义分享行为”、”需要响应深层链接跳转”等情况时,你就必须理解 Intent 的工作原理,才能在原生端正确实现功能。

更常见的是:你的应用中集成了一个第三方原生 SDK(比如推送 SDK、支付 SDK),它可能会通过 Intent 启动一个 Activity 或响应某个系统事件。如果你不理解 Intent 的匹配机制,就很难排查为什么 SDK 的功能在某些机型上不工作。

还有一个容易踩坑的领域是 Deep Link 和 URL Scheme——很多跨平台应用希望通过链接直接拉起应用并跳转到指定页面。这涉及到 Intent 过滤器的配置、URL 路由解析以及 Activity 启动模式的正确设置。配置出错是跨平台开发者最常遇到的问题之一。

本文将从头开始,系统性地解析 Android Intent 的方方面面,并以跨平台开发者的视角,聚焦那些最常遇到的场景和最常见的问题。

核心知识点拆解

什么是 Intent?

Intent 是 Android 中一个核心的运行时消息对象。它描述了你想执行的操作——”做什么”以及”需要哪些数据”。系统根据 Intent 中的信息,找到合适的组件来处理这个请求。

Intent 的重要字段:

  • Action:要执行的动作,如 ACTION_VIEWACTION_SENDACTION_PICK
  • Data:操作的数据 URI,如 tel:123456geo:39.9,116.4
  • Category:可选的额外信息,如 CATEGORY_BROWSABLE(表示该组件可以从浏览器打开)
  • Type:MIME 类型,如图片 image/png、文本 text/plain
  • Component:要启动的组件名(显式 Intent 使用)
  • Extras:键值对的额外数据
  • Flags:控制 Intent 的行为方式,如 FLAG_ACTIVITY_NEW_TASK

显式 Intent vs 隐式 Intent

这是 Intent 最核心的分类,也是理解 Android 组件通信的关键。

显式 Intent

显式 Intent 通过指定组件的完整类名来精确确定目标。它用于应用内部的组件跳转。

1
2
3
4
5
6
7
// 启动同一应用中的另一个 Activity
Intent intent = new Intent(this, DetailActivity.class);
startActivity(intent);

// 启动同一应用中的 Service
Intent serviceIntent = new Intent(this, MyService.class);
startService(serviceIntent);

特点:

  • 直接指定目标组件,不经过系统匹配
  • 性能更好,无匹配开销
  • 仅限应用内部或已知组件的调用
  • 安全性高,目标明确

隐式 Intent

隐式 Intent 不指定具体的目标组件,而是描述想要执行的操作,让系统根据 Intent 过滤器来匹配合适的组件。它用于跨应用的组件调用。

1
2
3
4
5
6
7
8
9
// 打开一个网址
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.example.com"));
startActivity(intent);

// 发送文本
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, "Hello from my app!");
startActivity(Intent.createChooser(intent, "分享给朋友"));

特点:

  • 不指定具体组件,系统根据 Intent 过滤器匹配
  • 可能存在多个匹配(弹出选择器)或没有匹配(崩溃)
  • 适合调用系统或其他应用的功能
  • 安全性需要关注,随意使用隐式 Intent 可能暴露敏感信息

对于跨平台开发者的实际意义

当你使用 Flutter 插件打开相机时,插件内部实际上发送了一个带有 ACTION_IMAGE_CAPTURE Action 的隐式 Intent,系统接收到这个 Intent 后在已安装的应用中找到一个可以处理”拍照”请求的应用(相机应用),并打开它。

1
2
3
4
5
6
// Flutter 中使用 image_picker 插件
final ImagePicker picker = ImagePicker();
final XFile? photo = await picker.pickImage(source: ImageSource.camera);
// 底层实现:plugin method channel → Android 原生
// → MediaStore.ACTION_IMAGE_CAPTURE 隐式 Intent
// → 启动系统相机应用

同理,当你调用 Share.share('text') 时,底层发送的是一个 ACTION_SEND 的隐式 Intent,系统弹出一个应用选择器让用户选择分享方式。

Action/Category/Data 匹配规则

Action 匹配

Action 描述了 Intent 要执行的动作。一个 Intent 必须包含 Action,系统通过 Action 来缩小候选组件的范围。

常见的系统 Action:

1
2
3
4
5
6
7
Intent.ACTION_VIEW       "android.intent.action.VIEW"      // 查看数据
Intent.ACTION_SEND       "android.intent.action.SEND"      // 发送数据
Intent.ACTION_EDIT       "android.intent.action.EDIT"      // 编辑数据
Intent.ACTION_DIAL       "android.intent.action.DIAL"      // 拨号
Intent.ACTION_PICK       "android.intent.action.PICK"      // 选择数据
Intent.ACTION_CALL       "android.intent.action.CALL"      // 直接拨打电话
Intent.ACTION_MAIN       "android.intent.action.MAIN"      // 入口点

Category 匹配

Category 为 Intent 提供额外的描述信息。一个 Intent 可以包含多个 Category,但接收方必须能处理所有指定 Category 中的每一个。

常见的 Category:

1
2
3
Intent.CATEGORY_DEFAULT    "android.intent.category.DEFAULT"    // 默认 Category
Intent.CATEGORY_BROWSABLE  "android.intent.category.BROWSABLE"  // 可从浏览器打开
Intent.CATEGORY_LAUNCHER   "android.intent.category.LAUNCHER"   // 应用入口

关键规则:系统在匹配隐式 Intent 时,会为 Intent 自动添加 CATEGORY_DEFAULT。这意味着,如果一个 Activity 在 Intent 过滤器中声明了接收某些 Action,但没有声明 CATEGORY_DEFAULT,则不会被隐式 Intent 匹配到。

这是跨平台开发者容易犯的错误之一:你在 Manifest 中配置了一个 Activity 接收 Deep Link,但是忘了加 DEFAULT Category,导致链接始终无法打开你的应用。

1
2
3
4
5
6
7
8
<activity android:name=".DeepLinkActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" /> <!-- 必须加 -->
        <category android:name="android.intent.category.BROWSABLE" /> <!-- 可选的,但建议加 -->
        <data android:scheme="https" android:host="myapp.com" />
    </intent-filter>
</activity>

Data 匹配

Data 部分指定了操作的数据 URI 和 MIME 类型。匹配规则如下:

1
2
3
4
5
6
<intent-filter>
    <data android:scheme="https"
          android:host="www.example.com"
          android:path="/product"
          android:mimeType="text/html" />
</intent-filter>

Data 匹配的字段:

  • scheme:协议,如 httpshttpcontentfile(必填,如果声明了 data 的话)
  • host:主机名,如 www.example.com
  • port:端口号
  • path / pathPrefix / pathPattern:路径匹配
  • mimeType:MIME 类型,如 text/plainimage/*

匹配逻辑

  • 如果 Intent 设置了 URI 但没有 Type,则只用 URI 的 scheme/host/path 匹配
  • 如果 Intent 设置了 Type 但没有 URI,则只用 Type 匹配
  • 如果两者都设置了,则两者都必须匹配

使用 Intent 启动其他应用

URL Scheme

URL Scheme 是 Android 和 iOS 都支持的跨应用通信方式。应用通过注册自己支持的 URL Scheme,使得其他应用或浏览器可以通过指定格式的 URL 来打开它。

AndroidManifest.xml 中的配置

1
2
3
4
5
6
7
8
<activity android:name=".SchemeActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="myapp" android:host="open" />
    </intent-filter>
</activity>

然后在浏览器或其他应用中,可以通过 myapp://open 来打开你的应用。

自定义 Scheme 的注意事项

  1. Scheme 命名要避免冲突(不要用 httphttps 等系统保留 Scheme)
  2. 建议使用反转域名命名,如 myapp://com.example.myapp://
  3. 支持传递参数:myapp://open?page=detail&id=123

Deep Link(深度链接)允许你通过普通的 HTTPS 链接来直接打开应用的特定页面。与 URL Scheme 不同,Deep Link 使用标准的 HTTP/HTTPS 协议链接。

1
2
3
4
5
6
7
8
9
10
11
<activity android:name=".DeepLinkActivity"
          android:autoVerify="true"> <!-- Android 6.0+ 支持 -->
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https"
              android:host="www.myapp.com"
              android:pathPrefix="/product" />
    </intent-filter>
</activity>

android:autoVerify="true" 是 Android 6.0(API 23)引入的功能,它要求应用发布者在其网站根目录下放置一个 assetlinks.json 文件,以验证网站所有权。验证通过后,系统会自动将匹配的链接路由到应用,而不需要弹窗询问用户。

assetlinks.json 文件格式

1
2
3
4
5
6
7
8
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.myapp",
    "sha256_cert_fingerprints": ["你的 SHA256 证书指纹"]
  }
}]

Flutter 中的 Deep Link 处理

在 Flutter 中,可以通过 go_routeruni_links 插件处理 Deep Link:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用 uni_links 插件
import 'package:uni_links/uni_links.dart';

void initDeepLinks() {
  // 初始化时获取链接
  getInitialLink().then((String? link) {
    if (link != null) handleDeepLink(link);
  });
  
  // 监听后续链接
  uriLinkStream.listen((Uri? uri) {
    if (uri != null) handleDeepLink(uri.toString());
  });
}

React Native 中的 Deep Link 处理

1
2
3
4
5
6
7
8
9
10
11
import { Linking } from 'react-native';

// 获取初始链接
Linking.getInitialURL().then((url) => {
  if (url) handleDeepLink(url);
});

// 监听后续链接
Linking.addEventListener('url', (event) => {
  handleDeepLink(event.url);
});

Activity 间传值

Intent putExtra

最基础的传值方式,使用 Intent.putExtra() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 发送方
Intent intent = new Intent(this, TargetActivity.class);
intent.putExtra("user_id", 12345);
intent.putExtra("user_name", "张三");
intent.putExtra("is_premium", true);
intent.putExtra("items", new ArrayList<>(Arrays.asList("a", "b", "c")));
startActivity(intent);

// 接收方(在 TargetActivity 中)
Intent intent = getIntent();
int userId = intent.getIntExtra("user_id", -1);
String userName = intent.getStringExtra("user_name");
boolean isPremium = intent.getBooleanExtra("is_premium", false);
ArrayList<String> items = intent.getStringArrayListExtra("items");

支持的数据类型:基本类型及其数组、String、Serializable、Parcelable 等。

Bundle

Bundle 是 Intent 传值的底层数据结构。上述的 putExtra 方法本质上是将数据存入了 Intent 的 Bundle 中。也可以显式使用 Bundle:

1
2
3
4
5
6
7
8
9
// 创建 Bundle 并放入数据
Bundle bundle = new Bundle();
bundle.putString("name", "Alice");
bundle.putInt("age", 28);
bundle.putBoolean("is_student", false);

Intent intent = new Intent(this, TargetActivity.class);
intent.putExtras(bundle);
startActivity(intent);

Bundle 在跨平台开发中的一个典型场景是:通过 MethodChannel 从 Flutter 侧传递复杂参数到原生端,原生端将这些参数封装成 Bundle 传递到目标 Activity。

startActivityForResult(旧方式)与 Activity Result API(新方式)

startActivityForResult(已废弃)

在 Android X 推出之前,Activity A 启动 Activity B 并接收返回结果的经典方式是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 发送方
Intent intent = new Intent(this, TargetActivity.class);
intent.putExtra("request_data", "hello");
startActivityForResult(intent, REQUEST_CODE_PICK);

// 接收结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_CODE_PICK) {
        if (resultCode == RESULT_OK) {
            String result = data.getStringExtra("result_data");
            // 处理返回结果
        }
    }
}

Activity Result API(推荐方式)

Android X Activity 1.2.0 引入了新的 Activity Result API,解决了旧方案的多个问题(如 Fragment 和 Activity 之间的代码耦合、旋转屏幕后数据丢失等):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 注册结果监听器
private final ActivityResultLauncher<Intent> pickLauncher = 
    registerForActivityResult(
        new ActivityResultContracts.StartActivityForResult(),
        result -> {
            if (result.getResultCode() == Activity.RESULT_OK) {
                Intent data = result.getData();
                if (data != null) {
                    String resultText = data.getStringExtra("result_data");
                    // 处理结果
                }
            }
        }
    );

// 启动 Activity
Intent intent = new Intent(this, TargetActivity.class);
pickLauncher.launch(intent);

对于常见的内置操作,Activity Result API 还提供了便捷的 Contract:

1
2
3
4
5
6
7
8
9
10
// 打开系统相机拍照
private final ActivityResultLauncher<Uri> cameraLauncher = 
    registerForActivityResult(
        new ActivityResultContracts.TakePicture(),
        success -> {
            if (success) {
                // 照片已保存到指定 URI
            }
        }
    );

对于跨平台开发者的意义

在 Flutter 中使用 Navigator.push 跳转页面时,Dart 层不需要关心 Android 层的 Activity 传值。但当你的 Flutter 应用需要调用原生能力(如打开系统相机、选择文件、扫码等)时,需要通过 MethodChannel 传递到原生端,而原生端则需要使用 Intent + Activity Result API 来实现这些功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Flutter 侧调用原生相机
final result = await platform.invokeMethod('openCamera');

// Android 原生侧实现
@Override
public void onMethodCall(MethodCall call, Result result) {
    if (call.method.equals("openCamera")) {
        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        photoUri = FileProvider.getUriForFile(context, authority, photoFile);
        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
        cameraLauncher.launch(cameraIntent);
    }
}

// 在 Activity Result 回调中返回结果给 Flutter
ActivityResultLauncher<Intent> cameraLauncher = registerForActivityResult(
    new ActivityResultContracts.StartActivityForResult(),
    activityResult -> {
        if (activityResult.getResultCode() == Activity.RESULT_OK) {
            Map<String, Object> response = new HashMap<>();
            response.put("path", photoUri.toString());
            pendingResult.success(response);
        } else {
            pendingResult.error("CAMERA_CANCELLED", "User cancelled", null);
        }
    }
);

PendingIntent 的使用场景

PendingIntent 是一个特殊的 Intent 包装器,它允许其他应用以你应用的权限和身份来执行某个 Intent。核心特征:PendingIntent 被创建后,其他组件(如系统服务、通知栏)可以代表你的应用执行它。

通知(Notification)

最常见的 PendingIntent 使用场景是通知点击事件。当用户点击通知时,系统通过创建的 PendingIntent 来启动你的 Activity。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 构建通知的点击 PendingIntent
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("from_notification", true);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);

PendingIntent pendingIntent = PendingIntent.getActivity(
    this,
    0,                              // requestCode
    intent,
    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);

// 构建通知
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
    .setContentTitle("新消息")
    .setContentText("你收到了一条新消息")
    .setSmallIcon(R.drawable.ic_notification)
    .setContentIntent(pendingIntent)
    .setAutoCancel(true)
    .build();

notificationManager.notify(NOTIFICATION_ID, notification);

桌面小部件(Widget)

小部件无法直接执行 Intent,需要通过 PendingIntent 来响应点击事件:

1
2
3
4
5
6
7
8
9
// 在小部件的 RemoteViews 中设置点击 PendingIntent
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra("action", "widget_click");
PendingIntent pendingIntent = PendingIntent.getActivity(
    context, 0, intent, 
    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
views.setOnClickPendingIntent(R.id.widget_button, pendingIntent);

Android 12 的 PendingIntent 安全性变化

Android 12(API 31)引入了一个重要的安全要求:所有 PendingIntent 必须显式声明可变性(mutability),否则会抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Android 12+ 必须设置 FLAG_IMMUTABLE 或 FLAG_MUTABLE
// 默认建议使用 FLAG_IMMUTABLE(更安全)
PendingIntent pendingIntent = PendingIntent.getActivity(
    context,
    0,
    intent,
    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE  // 必须加入
);

// 如果你的 PendingIntent 需要被修改(如动态更新 extra),才使用 MUTABLE
PendingIntent pendingIntent = PendingIntent.getActivity(
    context,
    0,
    intent,
    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE
);

Intent Flags 详解

Intent Flags 控制组件的启动方式。对于跨平台开发者来说,最常见的 Flags 使用场景是控制 Activity 的启动模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建一个新的 Task 并启动 Activity
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

// 如果要启动的 Activity 已存在,则将其带到前台
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);

// 清除当前 Task 中目标 Activity 之上的所有 Activity
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);

// 清除所有 Activity 并启动一个新的
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);

// 打开从通知发送的 Intent
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);

常见误区:从 Service 或 BroadcastReceiver 启动 Activity 时,必须添加 FLAG_ACTIVITY_NEW_TASK 标志,否则会抛出异常。

实战案例

案例一:Flutter 应用打开系统相册并返回结果

需求:Flutter 应用需要打开系统相册让用户选择一张图片,获取图片路径后上传。

实现方案(通过 MethodChannel)

Flutter 侧

1
2
3
4
5
6
7
8
9
10
11
class NativeImagePicker {
  static const _channel = MethodChannel('com.example.app/image');
  
  static Future<String?> pickImage() async {
    final imagePath = await _channel.invokeMethod('pickImage');
    return imagePath;
  }
}

// 使用
String? imagePath = await NativeImagePicker.pickImage();

Android 原生侧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 在 MainActivity 或 FlutterPlugin 中
public class ImagePickerPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
    private Result pendingResult;
    private ActivityResultLauncher<String> pickLauncher;
    
    @Override
    public void onMethodCall(MethodCall call, Result result) {
        if (call.method.equals("pickImage")) {
            pendingResult = result;
            // 使用 Activity Result API 打开系统相册
            Intent intent = new Intent(Intent.ACTION_PICK, 
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
            pickLauncher.launch("image/*");
        } else {
            result.notImplemented();
        }
    }
    
    private void registerLauncher() {
        pickLauncher = activity.registerForActivityResult(
            new ActivityResultContracts.GetContent(),
            uri -> {
                if (uri != null && pendingResult != null) {
                    pendingResult.success(uri.toString());
                } else if (pendingResult != null) {
                    pendingResult.error("PICK_CANCELLED", "User cancelled", null);
                }
                pendingResult = null;
            }
        );
    }
}

踩坑点

  1. 从 Flutter 调用原生 Activity 时,需要确保 registerForActivityResult 在 Activity 创建时注册,而不是在方法调用时
  2. 返回的 URI 需要经过路径转换(content:// → 文件路径),这是一个常见的兼容性问题
  3. 在 Android 10+ 上,返回的 URI 是基于 Scoped Storage 的 content URI,不能直接用 File(uri.path) 读取

需求:用户点击 https://myapp.com/product?id=123 链接时,直接打开应用的商品详情页。

实现方案

  1. AndroidManifest.xml 配置: ```xml
1
2
3
4
5
6
7
8
9
10
11
12
13
2. **assetlinks.json(放在网站根目录 `https://myapp.com/.well-known/assetlinks.json`)**:
```json
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.myapp",
    "sha256_cert_fingerprints": [
      "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:AD:A7:C7:FB:D5:7C:CE:67:DB:29:E0:1A:91:FB:5B:34"
    ]
  }
}]
  1. RN 侧处理: ```javascript import { Linking } from ‘react-native’;

function handleDeepLink(url) { if (url) { const { pathname, searchParams } = new URL(url); if (pathname.startsWith(‘/product’)) { const id = searchParams.get(‘id’); navigateToProductDetail(id); } } }

// 应用启动时获取初始链接 Linking.getInitialURL().then(url => handleDeepLink(url));

// 监听后续链接 const subscription = Linking.addEventListener(‘url’, event => { handleDeepLink(event.url); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
**踩坑点**:
1. `launchMode="singleTask"` 是必须的——如果没有设置,每次通过 Deep Link 打开应用都会创建一个新的 Activity 实例
2. SHA256 证书指纹必须是**正式发布签名**的指纹,调试签名不会被验证通过
3. 在 Android 12+ 上,如果应用从未安装过,系统会先打开浏览器而不是应用(即"首次点击不信任"策略)

### 案例三:使用 PendingIntent 实现通知跳转并传参

**需求**:推送通知点击后跳转到应用指定页面,并携带推送消息的参数。

**实现方案(Android 原生插件端)**:

```java
// 构建通知的 Intent
Intent intent = new Intent(context, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra("notification_payload", messagePayload); // 推送数据的 JSON 字符串

// 创建 PendingIntent(Android 12+ 必须添加 FLAG_IMMUTABLE)
PendingIntent pendingIntent = PendingIntent.getActivity(
    context,
    notificationId,
    intent,
    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);

Flutter 侧处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class NotificationService {
  static const _channel = MethodChannel('com.example.app/notification');
  
  static Future<Map<String, dynamic>?> getNotificationPayload() async {
    final payload = await _channel.invokeMethod('getNotificationPayload');
    return payload;
  }
}

// 在应用入口处处理
void main() {
  runApp(MyApp());
  WidgetsBinding.instance.addPostFrameCallback((_) async {
    final payload = await NotificationService.getNotificationPayload();
    if (payload != null) {
      // 跳转到指定页面
      navigateToPayloadPage(payload);
    }
  });
}

常见问题

Q1:调用 startActivity() 后应用崩溃,提示 “No Activity found to handle Intent”

原因:没有应用能够处理你发送的隐式 Intent。

解决方案

  1. 使用 PackageManager.queryIntentActivities() 检查是否有匹配的组件:
1
2
3
4
5
6
7
8
PackageManager pm = getPackageManager();
List<ResolveInfo> activities = pm.queryIntentActivities(intent, 0);
if (activities.size() > 0) {
    startActivity(intent);
} else {
    // 没有应用可以处理这个 Intent
    showToast("没有安装可处理此操作的应用");
}
  1. 在调用 startActivity() 前添加 resolveActivity 检查:
1
2
3
if (intent.resolveActivity(getPackageManager()) != null) {
    startActivity(intent);
}

排查清单

  1. Manifest 中是否添加了 DEFAULTBROWSABLE Category?
  2. 是否设置了 launchMode="singleTask"
  3. 在 Android 6.0+ 上,是否配置了 assetlinks.json 并放置在正确位置?
  4. 调试版本需要测试 Deep Link,可以使用 ADB 命令模拟:
1
adb shell am start -W -a android.intent.action.VIEW -d "https://myapp.com/product?id=123" com.myapp
  1. 检查 Deep Link 验证状态:
1
adb shell dumpsys package com.myapp | grep "autoVerify"

Q3:从后台通知点击启动应用时,传参丢失或不正确

原因

  • Activity 被系统回收后重建,Intent 数据被恢复
  • 使用了 Intent.FLAG_ACTIVITY_CLEAR_TOP 但目标 Activity 的 onNewIntent() 没有正确处理新 Intent

解决方案

  1. 始终重写 onNewIntent() 方法处理新传入的 Intent
  2. onCreate()onNewIntent() 中使用相同的提取逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    handleIntent(getIntent());
}

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    setIntent(intent); // 更新 Activity 的 Intent 引用
    handleIntent(intent);
}

private void handleIntent(Intent intent) {
    if (intent.hasExtra("notification_payload")) {
        String payload = intent.getStringExtra("notification_payload");
        // 处理推送数据
    }
}

Q4:Flutter 应用调用原生相机崩溃或返回空结果

常见原因

  1. Android 10+ 的 Scoped Storage:相机保存的 URI 不是实际文件路径,Flutter 侧无法直接读取
  2. FileProvider 配置错误:未正确配置 FileProvider 的 XML 文件
  3. 权限不足:未声明 CAMERAWRITE_EXTERNAL_STORAGE(Android 10 以下)权限

解决方案

  1. 使用 FileProvider.getUriForFile() 生成相机输出的 URI
  2. AndroidManifest.xml 中配置 FileProvider
  3. 返回结果的 URI 需要经过 copyFile 转换,确保 Flutter 侧能访问

Q5:PendingIntent 在 Android 12+ 上创建失败

原因:Android 12 要求所有 PendingIntent 必须显式声明可变性。

解决方案:始终添加 FLAG_IMMUTABLEFLAG_MUTABLE

1
2
3
4
5
6
7
// Android 12+ 安全
PendingIntent pendingIntent = PendingIntent.getActivity(
    context,
    0,
    intent,
    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);

总结

Intent 是 Android 组件通信的基石。对于跨平台开发者来说,理解 Intent 的核心概念和常见使用模式能够帮助你在以下场景中游刃有余:

  1. 调用系统功能:打开相册、相机、拨号盘、分享等——这些都是通过隐式 Intent 实现的
  2. 页面跳转和参数传递:通过显式 Intent 和 putExtra/Bundle 实现应用内导航
  3. Deep Link 集成:通过 Intent 过滤器和 URL Scheme/Deep Link 实现网页到应用的跳转
  4. 通知和小部件交互:通过 PendingIntent 实现系统组件与应用组件的通信
  5. 插件的原生功能实现:理解了 Intent 原理,编写 Flutter/RN 插件就不再是”复制粘贴”了

最后记住三条黄金法则:

  • 显式 Intent 用于内部跳转,隐式 Intent 用于外部调用
  • 所有接收隐式 Intent 的 Activity 必须包含 DEFAULT Category
  • Android 12+ 的 PendingIntent 必须声明可变性(FLAG_IMMUTABLE/FLAG_MUTABLE)
本文由作者按照 CC BY 4.0 进行授权