Каждый Android-разработчик сталкивался с необходимостью передать данные из одной Activity в другую. Эта тривиальная задача зачастую вынуждает нас писать не самый элегантный код.
Наконец, в 2020 году Google представила решение старой проблемы Activity Result API. Это мощный инструмент для обмена данными между активностями и запроса runtime permissions.
В данной статье мы разберёмся, как использовать новый API и какими преимуществами он обладает.
Чем плох onActivityResult()?
Роберт Мартин в книге Чистый код отмечает важность переиспользования кода принцип DRY или Dont repeat yourself, а также призывает проектировать компактные функции, которые выполняют лишь единственную операцию.
Проблема onActivityResult()
в том, что при его
использовании соблюдение подобных рекомендаций становится
практически невозможным. Убедимся в этом на примере простого
экрана, который запрашивает доступ к камере, делает фото и
открывает второй экран SecondActivity
. Пусть в
SecondActivity
мы передаём строку, а назад получаем
целое значение.
class OldActivity : AppCompatActivity(R.layout.a_main) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) vButtonCamera.setOnClickListener { when { checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> { // доступ к камере разрешен, открываем камеру startActivityForResult( Intent(MediaStore.ACTION_IMAGE_CAPTURE), PHOTO_REQUEST_CODE ) } shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> { // доступ к камере запрещен, нужно объяснить зачем нам требуется разрешение } else -> { // доступ к камере запрещен, запрашиваем разрешение requestPermissions( arrayOf(Manifest.permission.CAMERA), PHOTO_PERMISSIONS_REQUEST_CODE ) } } } vButtonSecondActivity.setOnClickListener { val intent = Intent(this, SecondActivity::class.java) .putExtra("my_input_key", "What is the answer?") startActivityForResult(intent, SECOND_ACTIVITY_REQUEST_CODE) } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when (requestCode) { PHOTO_REQUEST_CODE -> { if (resultCode == RESULT_OK && data != null) { val bitmap = data.extras?.get("data") as Bitmap // используем bitmap } else { // не удалось получить фото } } SECOND_ACTIVITY_REQUEST_CODE -> { if (resultCode == RESULT_OK && data != null) { val result = data.getIntExtra("my_result_extra") // используем result } else { // не удалось получить результат } } else -> super.onActivityResult(requestCode, resultCode, data) } } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { if (requestCode == PHOTO_PERMISSIONS_REQUEST_CODE) { when { grantResults[0] == PackageManager.PERMISSION_GRANTED -> { // доступ к камере разрешен, открываем камеру startActivityForResult( Intent(MediaStore.ACTION_IMAGE_CAPTURE), PHOTO_REQUEST_CODE ) } !shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> { // доступ к камере запрещен, пользователь поставил галочку Don't ask again. } else -> { // доступ к камере запрещен, пользователь отклонил запрос } } } else { super.onRequestPermissionsResult(requestCode, permissions, grantResults) } } companion object { private const val PHOTO_REQUEST_CODE = 1 private const val PHOTO_PERMISSIONS_REQUEST_CODE = 2 private const val SECOND_ACTIVITY_REQUEST_CODE = 3 }}
Очевидно, что метод onActivityResult()
нарушает
принцип единственной ответственности, ведь он отвечает и за
обработку результата фотографирования и за получение данных от
второй Activity. Да и выглядит этот метод уже довольно запутанно,
хоть мы и рассмотрели простой пример и опустили часть деталей.
Кроме того, если в приложении появится другой экран со схожей функциональностью, мы не сможем переиспользовать этот код и будем вынуждены его дублировать.
Используем Activity Result API
Новый API доступен начиная с AndroidX Activity
1.2.0-alpha02
и Fragment 1.3.0-alpha02
,
поэтому добавим актуальные версии соответствующих зависимостей в
build.gradle:
implementation 'androidx.activity:activity-ktx:1.3.0-alpha02'implementation 'androidx.fragment:fragment-ktx:1.3.0'
Применение Activity Result состоит из трех шагов:
Шаг 1. Создание контракта
Контракт это класс, реализующий интерфейс
ActivityResultContract<I,O>.
Где I
определяет тип входных данных, необходимых для запуска Activity, а
O
тип возвращаемого результата.
Для типовых задач можно воспользоваться реализациями из коробки:
PickContact
, TakePicture
,
RequestPermission
и другими. Полный список доступен
тут.
При создании контракта мы обязаны реализовать два его метода:
-
createIntent()
принимает входные данные и создает интент, который будет в дальнейшем запущен вызовом launch() -
parseResult()
отвечает за возврат результата, обработку resultCode и парсинг данных
Ещё один метод getSynchronousResult()
можно
переопределить в случае необходимости. Он позволяет сразу же, без
запуска Activity, вернуть результат, например, если получены
невалидные входные данные. Если подобное поведение не требуется,
метод по умолчанию возвращает null
.
Ниже представлен пример контракта, который принимает строку и запускает SecondActivity, ожидая от неё целое число:
class MySecondActivityContract : ActivityResultContract<String, Int?>() { override fun createIntent(context: Context, input: String?): Intent { return Intent(context, SecondActivity::class.java) .putExtra("my_input_key", input) } override fun parseResult(resultCode: Int, intent: Intent?): Int? = when { resultCode != Activity.RESULT_OK -> null else -> intent?.getIntExtra("my_result_key", 42) } override fun getSynchronousResult(context: Context, input: String?): SynchronousResult<Int?>? { return if (input.isNullOrEmpty()) SynchronousResult(42) else null }}
Шаг 2. Регистрация контракта
Следующий этап регистрация контракта в активности или фрагменте
с помощью вызова registerForActivityResult()
. В
параметры необходимо передать ActivityResultContract
и
ActivityResultCallback
. Коллбек сработает при
получении результата.
val activityLauncher = registerForActivityResult(MySecondActivityContract()) { result -> // используем result}
Регистрация контракта не запускает новую Activity
,
а лишь возвращает специальный объект
ActivityResultLauncher
, который нам понадобится
далее.
Шаг 3. Запуск контракта
Для запуска Activity остаётся вызвать launch()
на
объекте ActivityResultLauncher
, который мы получили на
предыдущем этапе.
vButton.setOnClickListener { activityLauncher.launch("What is the answer?")}
Важно!
Отметим несколько неочевидных моментов, которые необходимо учитывать:
-
Регистрировать контракты можно в любой момент жизненного цикла активности или фрагмента, но вот запустить его до перехода в состояние CREATED нельзя. Общепринятый подход регистрация контрактов как полей класса.
-
Не рекомендуется вызывать
registerForActivityResult()
внутри операторовif
иwhen
. Дело в том, что во время ожидания результата процесс приложения может быть уничтожен системой (например, при открытии камеры, которая требовательна к оперативной памяти). И если при восстановлении процесса мы не зарегистрируем контракт заново, результат будет утерян. -
Если запустить неявный интент, а операционная система не сможет найти подходящую Activity, выбрасывается исключение ActivityNotFoundException: No Activity found to handle Intent. Чтобы избежать такой ситуации, необходимо перед вызовом
launch()
или в методеgetSynchronousResult()
выполнить проверкуresolveActivity()
c помощьюPackageManager
.
Работа с runtime permissions
Другим полезным применением Activity Result API является запрос
разрешений. Теперь вместо вызовов
checkSelfPermission()
,
requestPermissions()
и
onRequestPermissionsResult()
, стало доступно
лаконичное и удобное решение контракты
RequestPermission
и
RequestMultiplePermissions
.
Первый служит для запроса одного разрешения, а второй сразу
нескольких. В колбеке RequestPermission
возвращает
true
, если доступ получен, и false
в
противном случае. RequestMultiplePermissions
вернёт
Map
, где ключ это название запрошенного разрешения, а
значение результат запроса.
В реальной жизни запрос разрешений выглядит несколько сложнее. В гайдлайнах Google мы видим следующую диаграмму:
Зачастую разработчики забывают о следующих нюансах при работе с runtime permissions:
-
Если пользователь ранее уже отклонял наш запрос, рекомендуется дополнительно объяснить, зачем приложению понадобилось данное разрешение (пункт 5a)
-
При отклонении запроса на разрешение (пункт 8b), стоит не только ограничить функциональность приложения, но и учесть случай, если пользователь поставил галочку Don't ask again
Обнаружить эти граничные ситуации можно при помощи вызова метода
shouldShowRequestPermissionRationale()
. Если он
возвращает true
перед запросом разрешения, то стоит
рассказать пользователю, как приложение будет использовать
разрешение. Если разрешение не выдано и
shouldShowRequestPermissionRationale()
возвращает
false
была выбрана опция Don't ask again, тогда стоит
попросить пользователя зайти в настройки и предоставить разрешение
вручную.
Реализуем запрос на доступ к камере согласно рассмотренной схеме:
class PermissionsActivity : AppCompatActivity(R.layout.a_main) { val singlePermission = registerForActivityResult(RequestPermission()) { granted -> when { granted -> { // доступ к камере разрешен, открываем камеру } !shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> { // доступ к камере запрещен, пользователь поставил галочку Don't ask again. } else -> { // доступ к камере запрещен, пользователь отклонил запрос } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) vButtonPermission.setOnClickListener { if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { // доступ к камере запрещен, нужно объяснить зачем нам требуется разрешение } else { singlePermission.launch(Manifest.permission.CAMERA) } } }}
Подводим итоги
Применим знания о новом API на практике и перепишем с их помощью экран из первого примера. В результате мы получим довольно компактный, легко читаемый и масштабируемый код:
class NewActivity : AppCompatActivity(R.layout.a_main) { val permission = registerForActivityResult(RequestPermission()) { granted -> when { granted -> { camera.launch() // доступ к камере разрешен, открываем камеру } !shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> { // доступ к камере запрещен, пользователь поставил галочку Don't ask again. } else -> { // доступ к камере запрещен } } } val camera = registerForActivityResult(TakePicturePreview()) { bitmap -> // используем bitmap } val custom = registerForActivityResult(MySecondActivityContract()) { result -> // используем result } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) vButtonCamera.setOnClickListener { if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { // объясняем пользователю, почему нам необходимо данное разрешение } else { permission.launch(Manifest.permission.CAMERA) } } vButtonSecondActivity.setOnClickListener { custom.launch("What is the answer?") } }}
Мы увидели недостатки обмена данными через onActivityResult(), узнали о преимуществах Activity Result API и научились использовать его на практике.
Новый API полностью стабилен, в то время как привычные
onRequestPermissionsResult()
,
onActivityResult()
и
startActivityForResult()
стали Deprecated. Самое время
вносить изменения в свои проекты!
Демо-приложение с различными примерами использования Activty Result API, в том числе работу с runtime permissions, можно найти в моем Github репозитории.