全面解析:如何在 Android 中优雅地使用 ActivityResultLauncher 替代已废弃的 startActivityForResult

在 Android 开发不断演进的世界中,保持技术栈的更新迭代不仅能提升应用的稳定性,还能让我们的代码更加优雅和易于维护。作为一名开发者,你一定对 INLINECODE747891da 这个方法非常熟悉。多年来,它一直是我们在 Activity 之间传递数据的基石。然而,随着 Android Jetpack 的推出,这一传统方法已经完成了它的历史使命。从 Android 11 (API 级别 30) 开始,INLINECODEbf8caa8f 和 INLINECODE892bf6c5 方法已被正式废弃,取而代之的是更加强大且灵活的 INLINECODE91e8fda9。

在这篇文章中,我们将深入探讨如何使用 ActivityResultLauncher 来彻底替代旧有的调用方式。我们将不仅学习“怎么写代码”,还会理解“为什么要这么写”,从而掌握处理 Activity 返回结果的最佳实践。

为什么我们需要做出改变?

在开始写代码之前,让我们先回顾一下过去使用 startActivityForResult() 时的痛点。你可能记得,为了处理来自相册、联系人或自定义 Activity 的返回结果,我们需要编写像意大利面条一样复杂的代码:

  • 请求代码管理混乱:我们必须定义常量(如 REQUEST_CODE_PICK)来区分不同的调用来源。如果项目庞大,这些常量散落在各处,极易冲突或混淆。
  • 生命周期耦合:所有的返回结果都汇集在 INLINECODE1f5876e9 这一个方法中。这意味着我们需要在一个方法里写大量的 INLINECODE68eaf998 或 INLINECODE0c83c277 语句来根据 INLINECODE3ad1f3bb 和 resultCode 分发逻辑,导致代码可读性极差。
  • 内存泄漏风险:在某些复杂的场景下,特别是在配合 Fragment 使用时,旧的方式在处理生命周期和结果传递时容易出现内存泄漏或结果丢失的问题。

为了解决这些问题,Google 引入了 INLINECODEf994576b 和 INLINECODEa82c4360。这套新机制将“如何发起请求”和“如何处理结果”紧密绑定在了一起,让我们能够以更函数式、更模块化的方式编写代码。

核心概念:理解 ActivityResultLauncher 与 Contract

在深入代码之前,我们需要先熟悉两个核心组件:

  • ActivityResultLauncher:这是用来启动请求的入口。你可以把它看作是一个改进版的“启动器”,它负责发起操作,并在操作完成后处理回调。
  • ActivityResultContract:这是一个规则定义类,它规定了“输入”是什么,“输出”是什么。例如,INLINECODE1377584d 这个 Contract 定义的输入是 INLINECODE8ba9bb31,输出是 INLINECODE167f82d2。Android 系统内置了许多常用的 Contract(如 INLINECODE23ddd8c6、TakePicture 等),我们也可以自定义。

步骤一:初始化 ActivityResultLauncher

使用新 API 的第一步,是注册一个 INLINECODEde6df427。这里有一个非常重要的生命周期规则:我们必须在 Activity 或 Fragment 的生命周期处于 CREATED 状态之前(通常是在 INLINECODE0e37c16b 中或成员变量初始化时)调用 registerForActivityResult

这样做的原因是,系统需要确保在 Activity 创建之前就绑定好结果监听器,这样即使 Activity 因配置更改(如屏幕旋转)而重建,也能正确接收到之前启动的结果,而不会丢失。

让我们来看一个具体的代码示例,我们将不再使用那个杂乱的 onActivityResult,而是使用 Lambda 表达式直接处理结果:

// 这是一个成员变量,用于持有我们的启动器
// 使用 registerForActivityResult 注册,并指定 Contract 类型
private ActivityResultLauncher activityResultLauncher = registerForActivityResult(
    // 第一个参数:Contract,这里使用标准的“启动 Activity 并获取结果”协议
    new ActivityResultContracts.StartActivityForResult(),
    // 第二个参数:回调函数,当结果返回时,这里的代码会被执行
    result -> {
        // 检查结果码是否为 OK(表示操作成功)
        if (result.getResultCode() == RESULT_OK) {
            // 获取返回的 Intent 数据
            Intent data = result.getData();
            
            // 处理数据,例如更新 UI
            if (data != null && data.getStringExtra("result") != null) {
                String receivedValue = data.getStringExtra("result");
                tvResult.setText(receivedValue);
                Log.i("MainActivity", "收到结果: " + receivedValue);
            }
        } else {
            Log.w("MainActivity", "用户取消了操作或返回失败");
        }
    }
);

步骤二:启动 Activity

注册好 Launcher 之后,我们就可以在任何需要的地方通过调用 INLINECODEb4c8fbb3 方法来启动目标 Activity 了。这一步取代了原来的 INLINECODEfe75df38。

btnResult.setOnClickListener(v -> {
    // 创建 Intent,指明我们要跳转到的目标 Activity
    Intent intent = new Intent(MainActivity.this, NextActivity.class);
    
    // 使用 Launcher 的 launch 方法启动
    // 你可以传递 Intent 作为参数(因为 StartActivityForResult Contract 接收 Intent)
    activityResultLauncher.launch(intent);
});

完整实战示例:从 Activity A 获取数据

为了让你能更直观地理解,我们将构建一个完整的场景:应用 A(主界面)点击按钮打开应用 B(输入界面),用户在 B 中输入内容后点击提交,A 收到内容并显示。

#### 1. 主界面 布局

这个界面包含一个用于显示结果的 TextView 和一个触发跳转的 Button。




    
    

    
    

    
    

#### 2. 主界面 Java 代码

注意我们如何简洁地处理逻辑,没有 Request Code,没有 Override 方法。

package com.example.modernandroid;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    // 成员变量:定义启动器
    // 我们可以直接在这里初始化,registerForActivityResult 是生命周期安全的
    private final ActivityResultLauncher startForResult = registerForActivityResult(
            new ActivityResultContracts.StartActivityForResult(),
            result -> {
                // 当 SecondActivity 结束并返回结果时,这里的代码会自动执行
                Log.d("MainActivity", "收到回调");
                
                if (result.getResultCode() == RESULT_OK) {
                    Intent data = result.getData();
                    // 这里的逻辑直接对应于这个启动器,非常清晰
                    if (data != null) {
                        String message = data.getStringExtra("key_result_data");
                        tvResult.setText(message);
                    }
                } else {
                    tvResult.setText("操作被用户取消");
                }
            }
    );

    private TextView tvResult;
    private Button btnResult;

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

        // 初始化视图
        tvResult = findViewById(R.id.tvResult);
        btnResult = findViewById(R.id.btnResult);

        // 设置点击事件
        btnResult.setOnClickListener(view -> {
            // 创建意图跳转到输入页面
            Intent intent = new Intent(MainActivity.this, SecondActivity.class);
            // 启动!不再需要请求码了
            startForResult.launch(intent);
        });
    }
}

#### 3. 被调用界面 的实现

这个页面的作用很简单:接收用户输入,然后将其发回。

package com.example.modernandroid;

import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import androidx.appcompat.app.AppCompatActivity;

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 假设这里有一个简单的布局,包含 EditText 和 Button
        // 为了演示方便,这里动态构建或假设已有 R.layout.activity_second
        setContentView(R.layout.activity_second); 

        EditText etInput = findViewById(R.id.etInput);
        Button btnSend = findViewById(R.id.btnSend);

        btnSend.setOnClickListener(v -> {
            String userInput = etInput.getText().toString();
            if (!userInput.isEmpty()) {
                Intent resultIntent = new Intent();
                // 放入数据
                resultIntent.putExtra("key_result_data", userInput);
                // 设置结果为 OK,并附带 Intent
                setResult(RESULT_OK, resultIntent);
                // 关闭 Activity,触发 MainActivity 的回调
                finish();
            } else {
                // 如果用户没输入,设置取消状态
                setResult(RESULT_CANCELED);
                finish();
            }
        });
    }
}

进阶场景:处理权限请求

INLINECODE0611e031 的强大之处在于它不仅用于 Activity 跳转,还统一了系统权限的处理。在旧版 API 中,我们需要检查 INLINECODE8a15b761,然后请求权限,最后在 onRequestPermissionsResult 中处理回调。现在,这一切都变得非常简单。

让我们看一个请求相机权限的例子:

// 专门用于请求权限的 Launcher
private final ActivityResultLauncher requestPermissionLauncher = 
    registerForActivityResult(
        // 使用 RequestPermission Contract,输入参数是权限名字符串
        new ActivityResultContracts.RequestPermission(),
        isGranted -> {
            if (isGranted) {
                // 权限被授予
                Log.i("Permission", "相机权限已获取");
                openCamera();
            } else {
                // 权限被拒绝
                Log.w("Permission", "用户拒绝了相机权限");
                // 这里可以提示用户去设置页面开启权限
            }
        }
);

// 在需要的地方调用
private void checkCameraPermission() {
    // 虽然 Launcher 会处理结果,但我们仍需先检查是否已有权限
    // 或者直接 launch,launcher 内部通常也会处理(视具体实现而定),最佳实践是先检查
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) 
            == PackageManager.PERMISSION_GRANTED) {
        openCamera();
    } else {
        // 启动权限请求流程
        requestPermissionLauncher.launch(Manifest.permission.CAMERA);
    }
}

进阶场景:选择多个文件

假设我们需要做一个相册选择功能,让用户挑选多张照片。旧的方式非常繁琐,而新 API 配合内置的 Contract 极其简单。

// 注意 Contract 的变化:PickMultipleVisualMedia
// 泛型  指定了返回的数据类型
private final ActivityResultLauncher pickMultipleMedia =
        registerForActivityResult(new ActivityResultContracts.GetMultipleContents(),
                uris -> {
                    // uris 是一个 List
                    if (!uris.isEmpty()) {
                        Log.d("Gallery", "选中了 " + uris.size() + " 个文件");
                        for (Uri uri : uris) {
                            Log.d("Gallery", "Uri: " + uri.toString());
                        }
                    }
                });

最佳实践与常见错误

在实际开发中,我们总结了一些经验,希望能帮助你避开陷阱:

#### 1. 绝对禁止:在 Fragment 中创建时使用 startActivity

这是一个非常常见的错误。如果你在 Fragment 内部调用 INLINECODEddbd3a4d,请务必使用 Fragment 的 INLINECODE99022ecd 上下文来注册,千万不要传入 Activity 的引用。如果在 Fragment 中使用 INLINECODEdd9669bc 但传入了 Activity 的生命周期拥有者,当 Fragment 重建时,你会发现回调没有被触发,或者因为 Activity 仍然是同一个实例而找不到对应的 Launcher。最佳做法是直接在 Fragment 类体内声明 INLINECODE6b1670db,它会自动绑定 Fragment 的生命周期。

#### 2. 生命周期时序

我们前面提到过,INLINECODE11845296 必须在 INLINECODEa3fa5984 之前完成。这意味着你不能在按钮的 INLINECODE8e6d3461 事件里去调用 INLINECODE325f75f5。如果你这样做了,应用会崩溃并抛出异常,告诉你 "LifecycleOwner … is attempting to register while current state is RESUMED"。正确的做法是始终将 Launcher 声明为成员变量。

#### 3. 性能优化

虽然 INLINECODEc70da2cb 非常轻量,但不要在 INLINECODEbd7cdad6 或 INLINECODE32824616 这种高频回调中重复创建它。一次初始化,多次 INLINECODE14c146f9 是最高效的方式。

总结

告别 INLINECODE16b53b23 标志着 Android 开发向着更加模块化、响应式和生命周期感知的方向迈进了一大步。通过使用 INLINECODEd24a0971,我们:

  • 摆脱了魔法数字:不再需要维护 INLINECODEb5e83604, INLINECODE85e4e45e 这种脆弱的常量。
  • 提升了代码可读性:结果处理的逻辑就在定义启动器的地方,上下文一目了然。
  • 统一了 API:无论是启动 Activity、请求权限还是拍照,都遵循同一套模式,降低了学习成本。

希望这篇文章能帮助你彻底掌握这一现代 Android 开发的必备技能。在未来的项目中,试着完全抛弃旧有的 onActivityResult,拥抱这种简洁、高效的新写法吧!如果你在实践过程中遇到任何问题,欢迎在评论区与我们交流。

准备好优化你的代码库了吗?让我们开始重构吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/30075.html
点赞
0.00 平均评分 (0% 分数) - 0