Получаем результат правильно (Часть 1). Activity Result API

от автора

Каждый 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *