Каждый Android-разработчик сталкивался с необходимостью передать данные из одной Activity в другую. Эта тривиальная задача зачастую вынуждает нас писать не самый элегантный код.
Наконец, в 2020 году Google представила решение старой проблемы — Activity Result API. Это мощный инструмент для обмена данными между активностями и запроса runtime permissions.
В данной статье мы разберёмся, как использовать новый API и какими преимуществами он обладает.
Чем плох onActivityResult()?
Роберт Мартин в книге “Чистый код” отмечает важность переиспользования кода — принцип DRY или Don’t 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 репозитории.
ссылка на оригинал статьи https://habr.com/ru/post/545934/
Добавить комментарий