A
第 13 章JAVA40 分钟

Intent 与页面跳转

掌握显式与隐式 Intent、IntentFilter、参数传递、ActivityResult API 与深链接 App Links,构建可互操作的页面跳转体系

学习目标

  • 区分显式 Intent 与隐式 Intent 的使用场景与构造方式
  • 熟练编写 IntentFilter 让 Activity 响应系统/其他 App 的跳转请求
  • 用 ActivityResult API 替代过时的 startActivityForResult,处理回传结果
  • 理解深链接 deeplink 与已验证 App Links 的差异并完成接入

学习目标

Intent 是 Android 中“启动 / 通信”的核心信使,几乎所有页面跳转、组件调用、跨 App 协作都依赖它。本章目标:

  • 在显式与隐式 Intent 间正确取舍;
  • 用 IntentFilter 让你的 Activity 响应特定动作;
  • 用现代 ActivityResult API 替代 startActivityForResult 处理“启动并等结果”的场景;
  • 接入 deeplink 与 App Links,让其他 App 或网页能跳转到你的具体页面。

显式 Intent vs 隐式 Intent

1. 显式 Intent:明确指定目标

public class MainActivity extends AppCompatActivity {

    private void openDetail(String id) {
        Intent intent = new Intent(this, DetailActivity.class);
        intent.putExtra("extra_id", id);
        intent.putExtra("extra_show_image", true);
        startActivity(intent);
    }
}

显式 Intent 内部使用 ComponentName,仅会在本 App 内查找目标,常用于内部页面跳转。

2. 隐式 Intent:声明动作让系统匹配

private void openUrl(String url) {
    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
    startActivity(intent);
}

private void shareText(String text) {
    Intent intent = new Intent(Intent.ACTION_SEND);
    intent.setType("text/plain");
    intent.putExtra(Intent.EXTRA_TEXT, text);
    startActivity(Intent.createChooser(intent, "分享到..."));
}

隐式 Intent 通过 action + category + data(URI + mimeType)匹配系统所有符合条件的组件,常用于跨 App 协作(打开网页、发邮件、分享)。

注意:自 Android 11(API 30)起,应用默认看不到其他 App 导出的组件。需要在 AndroidManifest.xml 中添加 <queries> 声明,否则隐式 Intent 可能匹配不到目标。

<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <queries>
        <!-- 允许查询浏览器 -->
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="https" />
        </intent>
        <!-- 允许查询分享文本的应用 -->
        <intent>
            <action android:name="android.intent.action.SEND" />
            <data android:mimeType="text/plain" />
        </intent>
    </queries>

    <application ...>
    </application>
</manifest>

IntentFilter 与清单声明

1. 在 Manifest 中声明过滤器

<activity
    android:name=".DetailActivity"
    android:exported="false">

    <!-- 隐式 Intent 过滤器 -->
    <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="detail"
              android:pathPrefix="/item" />
    </intent-filter>
</activity>

2. 三大要素

  • action:要执行的动作,如 VIEW / SEND / EDIT;
  • category:附加分类,隐式 Intent 至少需要包含 DEFAULT;
  • data:URL 与 mimeType 约束。

3. 启动 Activity 的隐式判断

private void openWebSafe(String url) {
    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
    if (intent.resolveActivity(getPackageManager()) != null) {
        startActivity(intent);
    } else {
        Toast.makeText(this, "未找到可处理该链接的应用", Toast.LENGTH_SHORT).show();
    }
}

putExtra 传参与 Parcelable

1. 基本类型传参

intent.putExtra("extra_id", "1001");
intent.putExtra("extra_count", 42);
intent.putExtra("extra_flag", true);
intent.putExtra("extra_list", new ArrayList<>(Arrays.asList("a", "b")));

接收方:

public class DetailActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);

        Intent intent = getIntent();
        String id = intent.getStringExtra("extra_id");
        int count = intent.getIntExtra("extra_count", 0);
        boolean flag = intent.getBooleanExtra("extra_flag", false);
        ArrayList<String> list = intent.getStringArrayListExtra("extra_list");
    }
}

2. 复杂对象:Parcelable

public class Article implements Parcelable {
    private String id;
    private String title;
    private long updateTime;

    public Article(String id, String title, long updateTime) {
        this.id = id;
        this.title = title;
        this.updateTime = updateTime;
    }

    protected Article(Parcel in) {
        id = in.readString();
        title = in.readString();
        updateTime = in.readLong();
    }

    public String getId() { return id; }
    public String getTitle() { return title; }
    public long getUpdateTime() { return updateTime; }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(id);
        dest.writeString(title);
        dest.writeLong(updateTime);
    }

    @Override
    public int describeContents() { return 0; }

    public static final Creator<Article> CREATOR = new Creator<Article>() {
        @Override
        public Article createFromParcel(Parcel in) { return new Article(in); }
        @Override
        public Article[] newArray(int size) { return new Article[size]; }
    };
}

// 启动时
intent.putExtra("extra_article", new Article("1001", "标题", System.currentTimeMillis()));

// 接收时
Article article = getIntent().getParcelableExtra("extra_article");

提示:传递大型 Parcelable 对象仍然受 Binder 事务大小限制(约 1MB),超过会抛 TransactionTooLargeException。建议传递 ID,目标页加载。

ActivityResult API:现代化回传结果

1. 旧 API 的痛点

startActivityForResult(intent, REQUEST_CODE) + onActivityResult(requestCode, resultCode, data) 存在以下问题:

  • 回调与启动代码分离,难维护;
  • requestCode 需要手动管理;
  • 在多 Fragment 时易冲突。

2. 新 API:ActivityResultLauncher

public class MainActivity extends AppCompatActivity {

    // 注册一个 launcher,回调内联到启动处
    private final ActivityResultLauncher<Intent> mDetailLauncher =
            registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
                    result -> {
                        if (result.getResultCode() == RESULT_OK && result.getData() != null) {
                            String returned = result.getData().getStringExtra("extra_returned");
                            // 处理回传结果
                        }
                    });

    private void openDetailForResult(String id) {
        Intent intent = new Intent(this, DetailActivity.class);
        intent.putExtra("extra_id", id);
        mDetailLauncher.launch(intent);
    }
}

3. 目标页回传结果

public class DetailActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);

        findViewById(R.id.btn_confirm).setOnClickListener(v -> {
            Intent data = new Intent();
            data.putExtra("extra_returned", "OK from detail");
            setResult(RESULT_OK, data);
            finish();
        });

        findViewById(R.id.btn_cancel).setOnClickListener(v -> {
            setResult(RESULT_CANCELED);
            finish();
        });
    }
}

4. 预置契约:简化常用场景

// 选图
private final ActivityResultLauncher<String> mPickImage = registerForActivityResult(
        new ActivityResultContracts.GetContent(),
        uri -> {
            if (uri != null) {
                ImageView iv = findViewById(R.id.iv_preview);
                iv.setImageURI(uri);
            }
        });

// 拍照
private final ActivityResultLauncher<Uri> mTakePhoto = registerForActivityResult(
        new ActivityResultContracts.TakePicture(),
        success -> {
            if (success) {
                // 图片已保存到指定 Uri
            }
        });

// 申请权限
private final ActivityResultLauncher<String> mRequestPermission = registerForActivityResult(
        new ActivityResultContracts.RequestPermission(),
        granted -> {
            if (granted) {
                // ...
            }
        });

// 调用:mPickImage.launch("image/*");

1. 自定义 Scheme

<activity
    android:name=".DetailActivity"
    android:exported="true">
    <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="detail" />
    </intent-filter>
</activity>

外部网页可以通过 <a href="myapp://detail?id=1001"> 触发跳转。在 DetailActivity 中解析参数:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_detail);

    Uri data = getIntent().getData();
    if (data != null) {
        String id = data.getQueryParameter("id");
        // 渲染对应内容
    }
}

自定义 scheme 简单但有缺陷:任意 App 都可声明同样的 scheme,存在被劫持风险。

2. App Links(已验证链接,Android 6+)

App Links 通过 HTTPS + 域名验证,确保链接只能由你的 App 打开,避免劫持。

步骤 1:声明 intent-filter

<activity
    android:name=".DetailActivity"
    android:exported="true">
    <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" />
    </intent-filter>
</activity>

步骤 2:上传 assetlinks.json

在 https://www.myapp.com/.well-known/assetlinks.json 上传如下文件(基于你的签名 SHA256):

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myapp",
      "sha256_cert_fingerprints": ["AB:CD:EF:..."]
    }
  }
]

步骤 3:测试

// 调试时手动触发验证
try {
    boolean verified = getPackageManager()
            .getPackageInfo(getPackageName(), PackageManager.GET_INTENT_FILTERS)
            .toString() != null; // 简化示例
} catch (Exception ignored) { }

// Android 12+ 可监听 VerificationOutcome
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        // ...
    }
};

调试时可用 adb shell pm verify-app-links --package com.example.myapp,并用 adb shell pm get-app-links com.example.myapp 查看验证状态。

常见坑与最佳实践

  1. 隐式 Intent 找不到目标却未判断 resolveActivity:会抛 ActivityNotFoundException。在 API 30+ 上即使有 App 能处理,没有 <queries> 声明也可能找不到。
  2. 传递大对象:Binder 事务上限约 1MB。传 ID 让目标页加载是更稳的做法。
  3. registerForActivityResult 必须在 STARTED 状态之前调用:通常放在字段初始化或 onCreate 中,不能在按钮点击回调里再注册。
  4. setResult 后忘了 finish():用户按返回键也会触发 RESULT_CANCELED,不要在按下“确认”时还没 setResult 又 finish。
  5. Intent.createChooser 与直接 startActivity:直接启动只让用户选择“默认应用”,chooser 会强制弹出选择面板,更适合分享类场景。
  6. App Links 验证失败:检查 assetlinks.json 是否 HTTPS 可访问、SHA256 是否为发布签名的指纹、是否有 autoVerify="true"。
  7. exported 属性:Android 12+ 强制要求所有含 intent-filter 的组件显式声明 android:exported,否则无法安装。
  8. FLAG_ACTIVITY_* 滥用:除非有明确需求,不要叠加 FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK,可能引发回退栈错乱。

章节小结

  • 显式 Intent 用于 App 内跳转,隐式 Intent 用于跨 App 协作;
  • IntentFilter 的 action/category/data 共同决定匹配范围,Android 11+ 别忘了加 <queries>;
  • 现代 ActivityResultLauncher 取代 startActivityForResult,回调内联、类型安全;
  • 自定义 scheme 简单但有劫持风险,App Links 通过 HTTPS + 域名验证确保链接专属。

下一章预告

第 14 章将进入 权限管理:normal 与 dangerous 权限的分类、清单声明、运行时申请(requestPermissions 与 ActivityResultContracts.RequestPermission),以及权限被拒后如何引导用户到系统设置。