Intent 与组件通信深度解析——跨平台开发者的 Android 原生交互指南
面向跨平台开发者的 Android Intent 完全指南,从显式/隐式 Intent 原理到打开系统相册、分享、Deep Link 跳转等 Native 功能调用,系统梳理 Flutter/RN/鸿蒙开发中与 Android 组件通信的必备知识。
一句话概括
Android Intent 是应用组件之间、应用与应用之间的消息传递机制——它就像 Android 世界的”快递系统”,无论是启动页面、打开相机、分享内容还是接收 Deep Link 跳转,背后都是 Intent 在完成消息的封装、匹配和传递。
背景与意义
如果你是 Flutter 或 React Native 开发者,你一定会遇到这样的需求:打开系统相册让用户选择图片、调用系统分享功能分享一段文本、或者从第三方应用跳转到你的应用某个具体页面。这些功能在 Android 平台上都是通过 Intent 完成的。
在 Flutter 中,你可能会用 image_picker 插件打开相机,在 React Native 中会用 react-native-image-picker 或 Share 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_VIEW、ACTION_SEND、ACTION_PICK - Data:操作的数据 URI,如
tel:123456、geo: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:协议,如
https、http、content、file(必填,如果声明了 data 的话) - host:主机名,如
www.example.com - port:端口号
- path / pathPrefix / pathPattern:路径匹配
- mimeType:MIME 类型,如
text/plain、image/*
匹配逻辑:
- 如果 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 的注意事项:
- Scheme 命名要避免冲突(不要用
http、https等系统保留 Scheme) - 建议使用反转域名命名,如
myapp://→com.example.myapp:// - 支持传递参数:
myapp://open?page=detail&id=123
Deep Link
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_router 或 uni_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;
}
);
}
}
踩坑点:
- 从 Flutter 调用原生 Activity 时,需要确保
registerForActivityResult在 Activity 创建时注册,而不是在方法调用时 - 返回的 URI 需要经过路径转换(content:// → 文件路径),这是一个常见的兼容性问题
- 在 Android 10+ 上,返回的 URI 是基于 Scoped Storage 的 content URI,不能直接用
File(uri.path)读取
案例二:React Native 应用响应 Deep Link 跳转
需求:用户点击 https://myapp.com/product?id=123 链接时,直接打开应用的商品详情页。
实现方案:
- 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"
]
}
}]
- 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。
解决方案:
- 使用
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("没有安装可处理此操作的应用");
}
- 在调用
startActivity()前添加resolveActivity检查:
1
2
3
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
}
Q2:为什么我的 Deep Link 在浏览器中点击后打不开应用?
排查清单:
- Manifest 中是否添加了
DEFAULT和BROWSABLECategory? - 是否设置了
launchMode="singleTask"? - 在 Android 6.0+ 上,是否配置了
assetlinks.json并放置在正确位置? - 调试版本需要测试 Deep Link,可以使用 ADB 命令模拟:
1
adb shell am start -W -a android.intent.action.VIEW -d "https://myapp.com/product?id=123" com.myapp
- 检查 Deep Link 验证状态:
1
adb shell dumpsys package com.myapp | grep "autoVerify"
Q3:从后台通知点击启动应用时,传参丢失或不正确
原因:
- Activity 被系统回收后重建,Intent 数据被恢复
- 使用了
Intent.FLAG_ACTIVITY_CLEAR_TOP但目标 Activity 的onNewIntent()没有正确处理新 Intent
解决方案:
- 始终重写
onNewIntent()方法处理新传入的 Intent - 在
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 应用调用原生相机崩溃或返回空结果
常见原因:
- Android 10+ 的 Scoped Storage:相机保存的 URI 不是实际文件路径,Flutter 侧无法直接读取
- FileProvider 配置错误:未正确配置 FileProvider 的 XML 文件
- 权限不足:未声明
CAMERA和WRITE_EXTERNAL_STORAGE(Android 10 以下)权限
解决方案:
- 使用
FileProvider.getUriForFile()生成相机输出的 URI - 在
AndroidManifest.xml中配置 FileProvider - 返回结果的 URI 需要经过 copyFile 转换,确保 Flutter 侧能访问
Q5:PendingIntent 在 Android 12+ 上创建失败
原因:Android 12 要求所有 PendingIntent 必须显式声明可变性。
解决方案:始终添加 FLAG_IMMUTABLE 或 FLAG_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 的核心概念和常见使用模式能够帮助你在以下场景中游刃有余:
- 调用系统功能:打开相册、相机、拨号盘、分享等——这些都是通过隐式 Intent 实现的
- 页面跳转和参数传递:通过显式 Intent 和 putExtra/Bundle 实现应用内导航
- Deep Link 集成:通过 Intent 过滤器和 URL Scheme/Deep Link 实现网页到应用的跳转
- 通知和小部件交互:通过 PendingIntent 实现系统组件与应用组件的通信
- 插件的原生功能实现:理解了 Intent 原理,编写 Flutter/RN 插件就不再是”复制粘贴”了
最后记住三条黄金法则:
- 显式 Intent 用于内部跳转,隐式 Intent 用于外部调用
- 所有接收隐式 Intent 的 Activity 必须包含
DEFAULTCategory - Android 12+ 的 PendingIntent 必须声明可变性(FLAG_IMMUTABLE/FLAG_MUTABLE)