Запуск другой операции, будь то в вашем приложении или в другом, не обязательно должен быть односторонним. Вы также можете запустить действие и получить результат. Например, ваше приложение может запустить приложение камеры и получить сделанную фотографию в качестве результата. Или вы можете запустить приложение "Контакты", чтобы пользователь выбрал контакт, а затем получить контактные данные в качестве результата.
Хотя базовые API startActivityForResult() и onActivityResult() доступны в классе Activity на всех уровнях API, Google настоятельно рекомендует использовать API Activity Result, представленные в классах AndroidX Activity и Fragment.
Activity Result API предоставляет компоненты для регистрации результата, запуска действия, которое его создает, и обработки результата после его отправки системой.
Регистрация обратного вызова для результата действия
При запуске действия для получения результата возможно (а в случае операций, требующих большого объема памяти, например при использовании камеры, почти наверняка), что ваш процесс и действие будут уничтожены из-за нехватки памяти.
По этой причине Activity Result API отделяет обратный вызов результата от места в коде, где вы запускаете другое действие. Поскольку обратный вызов результата должен быть доступен при повторном создании процесса и действия, обратный вызов должен регистрироваться безусловно при каждом создании действия, даже если логика запуска другого действия происходит только на основе пользовательского ввода или другой бизнес-логики.
В ComponentActivity или Fragment Activity Result API предоставляет API registerForActivityResult() для регистрации обратного вызова результата. registerForActivityResult() принимает ActivityResultContract и ActivityResultCallback и возвращает ActivityResultLauncher, который вы используете для запуска другого объекта activity.
ActivityResultContract определяет тип входных данных, необходимых для получения результата, а также тип выходных данных результата. API предоставляют контракты по умолчанию для базовых действий, таких как съемка фото, запрос разрешений и т. д. Вы также можете создать собственный контракт.
ActivityResultCallback – это интерфейс с одним методом onActivityResult(), который принимает объект выходного типа, определенного в 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 } });
Если у вас есть несколько вызовов результатов действия и вы используете разные контракты или хотите использовать отдельные обратные вызовы, вы можете вызвать registerForActivityResult() несколько раз, чтобы зарегистрировать несколько экземпляров ActivityResultLauncher. Вы должны вызывать registerForActivityResult() в одном и том же порядке для каждого создания фрагмента или действия, чтобы результаты передавались в правильный обратный вызов.
registerForActivityResult() можно безопасно вызывать до создания фрагмента или действия, что позволяет использовать его непосредственно при объявлении переменных-членов для возвращенных экземпляров ActivityResultLauncher.
Запуск действия для получения результата
Метод registerForActivityResult() регистрирует обратный вызов, но не запускает другое действие и не инициирует запрос результата. Это задача возвращенного экземпляра ActivityResultLauncher.
Если входные данные существуют, панель запуска принимает входные данные, соответствующие типу ActivityResultContract. Вызов launch() запускает процесс получения результата. Когда пользователь завершит последующее действие и вернется, будет выполнен метод onActivityResult() из ActivityResultCallback, как показано в следующем примере:
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/*"); } }); }
Перегруженная версия launch() позволяет передавать ActivityOptionsCompat в дополнение к входным данным.
Как получить результат действия в отдельном классе
Хотя классы ComponentActivity и Fragment реализуют интерфейс ActivityResultCaller, позволяющий использовать API registerForActivityResult(), вы также можете получать результат действия в отдельном классе, который не реализует ActivityResultCaller, напрямую используя ActivityResultRegistry.
Например, вы можете реализовать LifecycleObserver, который обрабатывает регистрацию контракта вместе с запуском средства запуска:
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(); } }); } }
При использовании API ActivityResultRegistry Google настоятельно рекомендует использовать API, которые принимают LifecycleOwner, поскольку LifecycleOwner автоматически удаляет зарегистрированный модуль запуска при уничтожении Lifecycle. Однако в случаях, когда LifecycleOwner недоступен, каждый класс ActivityResultLauncher позволяет вручную вызвать unregister() в качестве альтернативы.
Тест
По умолчанию registerForActivityResult() автоматически использует ActivityResultRegistry, предоставленный действием. Он также предоставляет перегрузку, которая позволяет передавать собственный экземпляр ActivityResultRegistry, который можно использовать для тестирования вызовов результатов действий без фактического запуска другого действия.
При тестировании фрагментов приложения вы предоставляете тестовый ActivityResultRegistry, используя FragmentFactory, чтобы передать ActivityResultRegistry в конструктор фрагмента.
Например, фрагмент, который использует контракт TakePicturePreview для получения уменьшенного изображения, может выглядеть следующим образом:
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; } // ... }
При создании тестового класса ActivityResultRegistry необходимо реализовать метод onLaunch(). Вместо вызова startActivityForResult() тестовая реализация может напрямую вызывать dispatchResult(), предоставляя точные результаты, которые вы хотите использовать в тесте:
val testRegistry = object : ActivityResultRegistry() {
override fun <I, O> onLaunch(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?
) {
dispatchResult(requestCode, expectedResult)
}
}
Полный тест создает ожидаемый результат, создает тестовый объект ActivityResultRegistry, передает его фрагменту, запускает средство запуска напрямую или с помощью других тестовых API, таких как Espresso, а затем проверяет результаты:
@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)
}
}
}
Создать собственный контракт
В ActivityResultContracts есть несколько готовых классов ActivityResultContract, но вы можете создавать собственные контракты, чтобы получить нужный вам типобезопасный API.
Для каждого ActivityResultContract необходимо определить классы ввода и вывода, используя Void в качестве типа ввода, если ввод не требуется (в Kotlin используйте Void? или Unit).
Каждый контракт должен реализовывать метод createIntent(), который принимает Context и входные данные и создает Intent, используемый с startActivityForResult().
Каждый контракт также должен реализовывать parseResult(), который создает выходные данные из заданного resultCode, например Activity.RESULT_OK или Activity.RESULT_CANCELED, и Intent.
Контракты могут дополнительно реализовывать getSynchronousResult(), если можно определить результат для заданных входных данных без вызова createIntent(), запуска другого действия и использования parseResult() для создания результата.
В следующем примере показано, как создать 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); } }
Если вам не нужен специальный контракт, вы можете использовать контракт StartActivityForResult. Это общий контракт, который принимает в качестве входных данных любой объект Intent и возвращает ActivityResult, позволяя извлекать resultCode и Intent в рамках обратного вызова, как показано в следующем примере:
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)); } }); }