Получайте результат от деятельности

Запуск другого действия, будь то в вашем приложении или из другого приложения, не обязательно должен быть односторонней операцией. Вы также можете начать действие и получить результат обратно. Например, ваше приложение может запустить приложение камеры и в результате получить сделанную фотографию. Или вы можете запустить приложение «Контакты», чтобы пользователь мог выбрать контакт, а затем получить в результате контактные данные.

Хотя базовые API-интерфейсы startActivityForResult() и onActivityResult() доступны в классе Activity на всех уровнях API, Google настоятельно рекомендует использовать API-интерфейсы Activity Result, представленные в классах AndroidX Activity и Fragment .

API-интерфейсы результатов действий предоставляют компоненты для регистрации результата, запуска действия, создающего результат, и обработки результата после его отправки системой.

Зарегистрируйте обратный вызов для результата активности

При запуске действия ради результата возможно (а в случае операций с интенсивным использованием памяти, таких как использование камеры, почти наверняка), что ваш процесс и ваша деятельность будут уничтожены из-за нехватки памяти.

По этой причине API результатов действия отделяют обратный вызов результата от места в вашем коде, где вы запускаете другое действие. Поскольку обратный вызов результата должен быть доступен при воссоздании вашего процесса и действия, обратный вызов должен быть безусловно зарегистрирован каждый раз, когда создается ваше действие, даже если логика запуска другого действия происходит только на основе пользовательского ввода или другой бизнес-логики.

В ComponentActivity или Fragment API результатов действия предоставляют API registerForActivityResult() для регистрации обратного вызова результата. registerForActivityResult() принимает ActivityResultContract и ActivityResultCallback и возвращает ActivityResultLauncher , который вы используете для запуска другого действия.

ActivityResultContract определяет тип ввода, необходимый для получения результата, а также тип вывода результата. API-интерфейсы предоставляют контракты по умолчанию для основных действий, таких как съемка изображения, запрос разрешений и т. д. Вы также можете создать собственный контракт .

ActivityResultCallback — это интерфейс с одним методом с методом onActivityResult() , который принимает объект типа вывода, определенного в ActivityResultContract :

Котлин

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

Ява

// 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 , как показано в следующем примере:

Котлин

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/*")
    }
}

Ява

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 , который обрабатывает регистрацию контракта вместе с запуском средства запуска:

Котлин

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()
        }
    }
}

Ява

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 для получения миниатюры изображения, может быть написан примерно так:

Котлин

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

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

    // ...
}

Ява

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 :

Котлин

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)
    }
}

Ява

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 как часть обратного вызова, как показано в следующем примере:

Котлин

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))
    }
}

Ява

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));
        }
    });
}