Nhận kết quả từ một hoạt động

Việc bắt đầu một hoạt động khác (dù là hoạt động trong ứng dụng hay từ ứng dụng khác) không nhất thiết phải là quá trình một chiều. Bạn cũng có thể bắt đầu một hoạt động và nhận lại kết quả. Ví dụ: Ứng dụng của bạn có thể khởi động ứng dụng máy ảnh rồi sau đó nhận ảnh đã chụp. Hoặc bạn có thể khởi động ứng dụng Danh bạ để người dùng chọn một người liên hệ, nhờ đó bạn sẽ nhận được thông tin liên hệ.

Tuy các API cơ bản startActivityForResult()onActivityResult() có sẵn trên lớp Activity ở tất cả cấp độ API, bạn nên sử dụng Activity Result API được giới thiệu trong lớp ActivityFragment của AndroidX.

Activity Result API cung cấp các thành phần để đăng ký một kết quả, khởi động kết quả và xử lý kết quả sau khi hệ thống gửi đi.

Đăng ký lệnh gọi lại cho kết quả hoạt động

Khi bắt đầu một hoạt động cho một kết quả, có thể (và trong trường hợp những hoạt động cần nhiều bộ nhớ như sử dụng máy ảnh) gần như chắc chắn là quá trình và hoạt động của bạn sẽ bị huỷ bỏ do bộ nhớ sắp hết.

Vì lý do này, Activity Result API tách biệt lệnh gọi lại kết quả từ vị trí trong mã nơi bạn chạy hoạt động khác. Vì cần phải có sẵn lệnh gọi lại kết quả khi quá trình và hoạt động được tạo lại, nên lệnh gọi lại phải được đăng ký vô điều kiện mỗi khi tạo hoạt động, ngay cả khi logic để chạy hoạt động khác chỉ diễn ra dựa trên hoạt động đầu vào của người dùng hoặc logic kinh doanh khác.

Khi ở trong ComponentActivity hoặc Fragment, API Activity Result sẽ cung cấp một API registerForActivityResult() để đăng ký lệnh gọi lại kết quả. registerForActivityResult() lấy ActivityResultContractActivityResultCallback rồi trả về một ActivityResultLauncher mà bạn sẽ sử dụng để chạy hoạt động khác.

ActivityResultContract xác định loại đầu vào cần thiết để tạo ra kết quả cùng với loại đầu ra của kết quả. API này cung cấp hợp đồng mặc định cho các thao tác theo ý định cơ bản như chụp ảnh, yêu cầu quyền, v.v. Bạn cũng có thể tạo hợp đồng tuỳ chỉnh.

ActivityResultCallback là giao diện phương thức duy nhất có phương thức onActivityResult() lấy đối tượng của loại đầu ra được xác định trong ActivityResultContract:

Kotlin

val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
    // Handle the returned Uri
}

Java

// GetContent creates an ActivityResultLauncher<String> to let you pass
// in the mime type you want to let the user select
ActivityResultLauncher<String> mGetContent = registerForActivityResult(new GetContent(),
    new ActivityResultCallback<Uri>() {
        @Override
        public void onActivityResult(Uri uri) {
            // Handle the returned Uri
        }
});

Nếu có nhiều lệnh gọi kết quả hoạt động, đồng thời bạn đang sử dụng nhiều hợp đồng khác nhau hoặc muốn có lệnh gọi lại riêng biệt, thì bạn có thể gọi registerForActivityResult() nhiều lần để đăng ký nhiều thực thể ActivityResultLauncher. Bạn phải gọi registerForActivityResult() theo cùng một thứ tự cho mỗi lượt tạo mảnh hoặc hoạt động để đảm bảo kết quả đang truyền được gửi tới đúng lệnh gọi lại.

Bạn có thể an toàn gọi lệnh registerForActivityResult() trước khi tạo mảnh hoặc hoạt động, nhờ vậy có thể sử dụng trực tiếp lệnh đó khi khai báo biến thành phần cho các thực thể ActivityResultLauncher đã trả về.

Khởi chạy một hoạt động cho kết quả

Tuy đăng ký lệnh gọi lại của bạn nhưng registerForActivityResult() không chạy hoạt động khác và bắt đầu yêu cầu kết quả. Thay vào đó, đây là trách nhiệm của thực thể ActivityResultLauncher được trả về.

Nếu có giá trị đầu vào, trình chạy sẽ lấy giá trị đầu vào khớp với loại của ActivityResultContract. Hoạt động gọi launch() sẽ bắt đầu quy trình tạo kết quả. Khi người dùng thực hiện xong hoạt động sau đó và trả về, thì onActivityResult() từ ActivityResultCallback sẽ được thực thi, như trong ví dụ sau:

Kotlin

val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
    // Handle the returned Uri
}

override fun onCreate(savedInstanceState: Bundle?) {
    // ...

    val selectButton = findViewById<Button>(R.id.select_button)

    selectButton.setOnClickListener {
        // Pass in the mime type you want to let the user select
        // as the input
        getContent.launch("image/*")
    }
}

Java

ActivityResultLauncher<String> mGetContent = registerForActivityResult(new GetContent(),
    new ActivityResultCallback<Uri>() {
        @Override
        public void onActivityResult(Uri uri) {
            // Handle the returned Uri
        }
});

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    // ...

    Button selectButton = findViewById(R.id.select_button);

    selectButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View view) {
            // Pass in the mime type you want to let the user select
            // as the input
            mGetContent.launch("image/*");
        }
    });
}

Phiên bản nạp chồng của launch() cho phép bạn truyền thêm ActivityOptionsCompat cho giá trị đầu vào.

Nhận kết quả hoạt động trong một lớp riêng

Tuy lớp ComponentActivityFragment triển khai giao diện ActivityResultCaller để cho phép bạn sử dụng registerForActivityResult() API, nhưng bạn cũng có thể nhận được kết quả hoạt động trong một lớp riêng biệt chứ không triển khai ActivityResultCaller bằng cách trực tiếp sử dụng ActivityResultRegistry.

Ví dụ: Có thể bạn nên triển khai LifecycleObserver, giúp xử lý việc đăng ký hợp đồng cùng với khởi động trình chạy:

Kotlin

class MyLifecycleObserver(private val registry : ActivityResultRegistry)
        : DefaultLifecycleObserver {
    lateinit var getContent : ActivityResultLauncher<String>

    override fun onCreate(owner: LifecycleOwner) {
        getContent = registry.register("key", owner, GetContent()) { uri ->
            // Handle the returned Uri
        }
    }

    fun selectImage() {
        getContent.launch("image/*")
    }
}

class MyFragment : Fragment() {
    lateinit var observer : MyLifecycleObserver

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...

        observer = MyLifecycleObserver(requireActivity().activityResultRegistry)
        lifecycle.addObserver(observer)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val selectButton = view.findViewById<Button>(R.id.select_button)

        selectButton.setOnClickListener {
            // Open the activity to select an image
            observer.selectImage()
        }
    }
}

Java

class MyLifecycleObserver implements DefaultLifecycleObserver {
    private final ActivityResultRegistry mRegistry;
    private ActivityResultLauncher<String> mGetContent;

    MyLifecycleObserver(@NonNull ActivityResultRegistry registry) {
        mRegistry = registry;
    }

    public void onCreate(@NonNull LifecycleOwner owner) {
        // ...

        mGetContent = mRegistry.register(“key”, owner, new GetContent(),
            new ActivityResultCallback<Uri>() {
                @Override
                public void onActivityResult(Uri uri) {
                    // Handle the returned Uri
                }
            });
    }

    public void selectImage() {
        // Open the activity to select an image
        mGetContent.launch("image/*");
    }
}

class MyFragment extends Fragment {
    private MyLifecycleObserver mObserver;

    @Override
    void onCreate(Bundle savedInstanceState) {
        // ...

        mObserver = new MyLifecycleObserver(requireActivity().getActivityResultRegistry());
        getLifecycle().addObserver(mObserver);
    }

    @Override
    void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        Button selectButton = findViewById(R.id.select_button);
        selectButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mObserver.selectImage();
            }
        });
    }
}

Đặc biệt, khi sử dụng ActivityResultRegistry API, bạn nên sử dụng API lấy LifecycleOwner, vì LifecycleOwner sẽ tự động xoá trình chạy đã đăng ký khi Lifecycle bị huỷ. Tuy nhiên, trong trường hợp không có LifecycleOwner, thì giải pháp thay thế là mỗi lớp ActivityResultLauncher sẽ cho phép bạn gọi unregister() theo cách thủ công.

Kiểm thử

Theo mặc định, registerForActivityResult() sẽ tự động sử dụng ActivityResultRegistry do hoạt động cung cấp. API này cũng cung cấp phương thức nạp chồng cho phép bạn truyền thực thể ActivityResultRegistry riêng mà bạn có thể sử dụng để kiểm thử lệnh gọi kết quả hoạt động mà không thực sự khởi chạy hoạt động khác.

Khi kiểm thử mảnh của ứng dụng, bạn sẽ cung cấp ActivityResultRegistry kiểm thử bằng cách sử dụng FragmentFactory để truyền ActivityResultRegistry đến hàm khởi tạo của mảnh.

Ví dụ: Một mảnh sử dụng hợp đồng TakePicturePreview để tải hình thu nhỏ của hình ảnh có thể được ghi theo cách tương tự như sau:

Kotlin

class MyFragment(
    private val registry: ActivityResultRegistry
) : Fragment() {
    val thumbnailLiveData = MutableLiveData<Bitmap?>

    val takePicture = registerForActivityResult(TakePicturePreview(), registry) {
        bitmap: Bitmap? -> thumbnailLiveData.setValue(bitmap)
    }

    // ...
}

Java

public class MyFragment extends Fragment {
    private final ActivityResultRegistry mRegistry;
    private final MutableLiveData<Bitmap> mThumbnailLiveData = new MutableLiveData();
    private final ActivityResultLauncher<Void> mTakePicture =
        registerForActivityResult(new TakePicturePreview(), mRegistry, new ActivityResultCallback<Bitmap>() {
            @Override
            public void onActivityResult(Bitmap thumbnail) {
                mThumbnailLiveData.setValue(thumbnail);
            }
        });

    public MyFragment(@NonNull ActivityResultRegistry registry) {
        super();
        mRegistry = registry;
    }

    @VisibleForTesting
    @NonNull
    ActivityResultLauncher<Void> getTakePicture() {
        return mTakePicture;
    }

    @VisibleForTesting
    @NonNull
    LiveData<Bitmap> getThumbnailLiveData() {
        return mThumbnailLiveData;
    }

    // ...
}

Khi tạo một ActivityResultRegistry kiểm thử cụ thể, bạn phải triển khai phương thức onLaunch(). Thay vì gọi startActivityForResult(), phương thức triển khai kiểm thử có thể gọi trực tiếp dispatchResult(), cung cấp kết quả chính xác mà bạn muốn sử dụng trong chương trình kiểm thử của mình:

val testRegistry = object : ActivityResultRegistry() {
    override fun <I, O> onLaunch(
            requestCode: Int,
            contract: ActivityResultContract<I, O>,
            input: I,
            options: ActivityOptionsCompat?
    ) {
        dispatchResult(requestCode, expectedResult)
    }
}

Chương trình kiểm thử hoàn chỉnh sẽ tạo ra kết quả dự kiến, xây dựng ActivityResultRegistry kiểm thử, truyền thông tin vào mảnh, kích hoạt trình chạy trực tiếp hoặc sử dụng các API kiểm thử khác như Espresso, rồi xác minh kết quả:

@Test
fun activityResultTest {
    // Create an expected result Bitmap
    val expectedResult = Bitmap.createBitmap(1, 1, Bitmap.Config.RGBA_F16)

    // Create the test ActivityResultRegistry
    val testRegistry = object : ActivityResultRegistry() {
            override fun <I, O> onLaunch(
            requestCode: Int,
            contract: ActivityResultContract<I, O>,
            input: I,
            options: ActivityOptionsCompat?
        ) {
            dispatchResult(requestCode, expectedResult)
        }
    }

    // Use the launchFragmentInContainer method that takes a
    // lambda to construct the Fragment with the testRegistry
    with(launchFragmentInContainer { MyFragment(testRegistry) }) {
            onFragment { fragment ->
                // Trigger the ActivityResultLauncher
                fragment.takePicture()
                // Verify the result is set
                assertThat(fragment.thumbnailLiveData.value)
                        .isSameInstanceAs(expectedResult)
            }
    }
}

Tạo hợp đồng tuỳ chỉnh

Tuy ActivityResultContracts có chứa một số lớp ActivityResultContract dựng sẵn để sử dụng, nhưng bạn có thể cung cấp hợp đồng riêng mang đến API an toàn theo đúng loại bạn cần.

Mỗi ActivityResultContract đòi hỏi các lớp đầu vào và đầu ra đã được xác định, sử dụng Void làm loại đầu vào nếu không có bất cứ yêu cầu nào về đầu vào (trong Kotlin, hãy sử dụng Void? hoặc Unit).

Mỗi hợp đồng phải triển khai phương thức createIntent(). Phương thức này sẽ lấy Context và giá trị nhập cũng như tạo Intent sẽ được sử dụng cùng startActivityForResult().

Mỗi hợp đồng cũng phải triển khai parseResult() để tạo dữ liệu đầu ra từ resultCode cho trước (ví dụ: Activity.RESULT_OK hoặc Activity.RESULT_CANCELED, cũng như Intent).

Hợp đồng có thể tuỳ ý triển khai getSynchronousResult() nếu có thể xác định kết quả cho một giá trị đầu vào cho trước mà không cần gọi createIntent(), bắt đầu hoạt động khác và sử dụng parseResult() để tạo kết quả.

Ví dụ sau đây cho thấy cách tạo ActivityResultContract:

Kotlin

class PickRingtone : ActivityResultContract<Int, Uri?>() {
    override fun createIntent(context: Context, ringtoneType: Int) =
        Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
            putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringtoneType)
        }

    override fun parseResult(resultCode: Int, result: Intent?) : Uri? {
        if (resultCode != Activity.RESULT_OK) {
            return null
        }
        return result?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
    }
}

Java

public class PickRingtone extends ActivityResultContract<Integer, Uri> {
    @NonNull
    @Override
    public Intent createIntent(@NonNull Context context, @NonNull Integer ringtoneType) {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringtoneType.intValue());
        return intent;
    }

    @Override
    public Uri parseResult(int resultCode, @Nullable Intent result) {
        if (resultCode != Activity.RESULT_OK || result == null) {
            return null;
        }
        return result.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
    }
}

Nếu không cần hợp đồng tuỳ chỉnh, bạn có thể sử dụng hợp đồng StartActivityForResult. Đây là một hợp đồng khung, lấy bất cứ Intent nào làm giá trị đầu vào và trả về ActivityResult, cho phép bạn trích xuất resultCodeIntent như một phần của lệnh gọi lại, như trong ví dụ sau:

Kotlin

val startForResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult ->
    if (result.resultCode == Activity.RESULT_OK) {
        val intent = result.data
        // Handle the Intent
    }
}

override fun onCreate(savedInstanceState: Bundle) {
    // ...

    val startButton = findViewById(R.id.start_button)

    startButton.setOnClickListener {
        // Use the Kotlin extension in activity-ktx
        // passing it the Intent you want to start
        startForResult.launch(Intent(this, ResultProducingActivity::class.java))
    }
}

Java

ActivityResultLauncher<Intent> mStartForResult = registerForActivityResult(new StartActivityForResult(),
        new ActivityResultCallback<ActivityResult>() {
    @Override
    public void onActivityResult(ActivityResult result) {
        if (result.getResultCode() == Activity.RESULT_OK) {
            Intent intent = result.getData();
            // Handle the Intent
        }
    }
});

@Override
public void onCreate(@Nullable savedInstanceState: Bundle) {
    // ...

    Button startButton = findViewById(R.id.start_button);

    startButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View view) {
            // The launcher with the Intent you want to start
            mStartForResult.launch(new Intent(this, ResultProducingActivity.class));
        }
    });
}