Получаем результат правильно (Часть 1). Activity Result API
Каждый Android-разработчик сталкивался с необходимостью передать данные из одной Activity в другую. Эта тривиальная задача зачастую вынуждает нас писать не самый элегантный код.
Наконец, в 2020 году Google представила решение старой проблемы — Activity Result API. Это мощный инструмент для обмена данными между активностями и запроса runtime permissions.
В данной статье мы разберёмся, как использовать новый API и какими преимуществами он обладает.
Чем плох onActivityResult()?
Роберт Мартин в книге “Чистый код” отмечает важность переиспользования кода — принцип DRY или Don’t repeat yourself, а также призывает проектировать компактные функции, которые выполняют лишь единственную операцию.
Проблема onActivityResult() в том, что при его использовании соблюдение подобных рекомендаций становится практически невозможным. Убедимся в этом на примере простого экрана, который запрашивает доступ к камере, делает фото и открывает второй экран — SecondActivity . Пусть в SecondActivity мы передаём строку, а назад получаем целое значение.
Очевидно, что метод onActivityResult() нарушает принцип единственной ответственности, ведь он отвечает и за обработку результата фотографирования и за получение данных от второй Activity. Да и выглядит этот метод уже довольно запутанно, хоть мы и рассмотрели простой пример и опустили часть деталей.
Кроме того, если в приложении появится другой экран со схожей функциональностью, мы не сможем переиспользовать этот код и будем вынуждены его дублировать.
Используем Activity Result API
Новый API доступен начиная с AndroidX Activity 1.2.0-alpha02 и Fragment 1.3.0-alpha02 , поэтому добавим актуальные версии соответствующих зависимостей в build.gradle:
Применение Activity Result состоит из трех шагов:
Шаг 1. Создание контракта
Контракт — это класс, реализующий интерфейс ActivityResultContract . Где I определяет тип входных данных, необходимых для запуска Activity, а O — тип возвращаемого результата.
Для типовых задач можно воспользоваться реализациями “из коробки”: PickContact , TakePicture , RequestPermission и другими. Полный список доступен тут.
При создании контракта мы обязаны реализовать два его метода:
createIntent() — принимает входные данные и создает интент, который будет в дальнейшем запущен вызовом launch()
parseResult() — отвечает за возврат результата, обработку resultCode и парсинг данных
Ещё один метод — getSynchronousResult() — можно переопределить в случае необходимости. Он позволяет сразу же, без запуска Activity, вернуть результат, например, если получены невалидные входные данные. Если подобное поведение не требуется, метод по умолчанию возвращает null .
Ниже представлен пример контракта, который принимает строку и запускает SecondActivity, ожидая от неё целое число:
Шаг 2. Регистрация контракта
Следующий этап — регистрация контракта в активности или фрагменте с помощью вызова registerForActivityResult() . В параметры необходимо передать ActivityResultContract и ActivityResultCallback . Коллбек сработает при получении результата.
Регистрация контракта не запускает новую Activity , а лишь возвращает специальный объект ActivityResultLauncher , который нам понадобится далее.
Шаг 3. Запуск контракта
Для запуска Activity остаётся вызвать launch() на объекте ActivityResultLauncher , который мы получили на предыдущем этапе.
Важно!
Отметим несколько неочевидных моментов, которые необходимо учитывать:
Регистрировать контракты можно в любой момент жизненного цикла активности или фрагмента, но вот запустить его до перехода в состояние 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”, тогда стоит попросить пользователя зайти в настройки и предоставить разрешение вручную.
Реализуем запрос на доступ к камере согласно рассмотренной схеме:
Подводим итоги
Применим знания о новом API на практике и перепишем с их помощью экран из первого примера. В результате мы получим довольно компактный, легко читаемый и масштабируемый код:
Мы увидели недостатки обмена данными через onActivityResult(), узнали о преимуществах Activity Result API и научились использовать его на практике.
Новый API полностью стабилен, в то время как привычные onRequestPermissionsResult() , onActivityResult() и startActivityForResult() стали Deprecated. Самое время вносить изменения в свои проекты!
Демо-приложение с различными примерами использования Activty Result API, в том числе работу с runtime permissions, можно найти в моем Github репозитории.
Источник
Полный список
— разбираемся, зачем нужны requestCode и resultCode в onActivityResult
На прошлом уроке мы поверхностно рассмотрели, как вызвать Activity, и как сделать так, чтобы она вернула результат. Рассмотрим немного подробней этот механизм. Создадим приложение, которое будет вызывать два разных Activity и получать от них результат. Как мы помним, результат приходит в метод onActivityResult. И requestCode используется, чтобы отличать друг от друга пришедшие результаты. А resultCode – позволяет определить успешно прошел вызов или нет.
Project name: P0301_ActivityResult
Build Target: Android 2.3.3
Application name: ActivityResult
Package name: ru.startandroid.develop.p0301activityresult
Create Activity: MainActivity
Нарисуем экран в main.xml:
На экране TextView с текстом. И две кнопки для выбора цвета шрифта и выравнивания текста в TextView. Нажатие на кнопку будет вызывать Activity для выбора и получать обратно результат.
Давайте начнем кодить в MainActivity.java:
Определили экранные элементы, прописали обработчик кнопкам и пока остановимся на этом.
Создадим два других Activity. Начнем с Activity для выбора цвета. Создадим layout-файл color.xml:
Создаем класс ColorActivity. ColorActivity.java:
Как обычно определяем элементы, присваиваем обработчик кнопкам и реализуем onClick. В onClick мы создаем Intent, затем определяем, кнопка с каким цветом была нажата и помещаем в Intent объект с именем color и значением цвета. Ставим статус RESULT_OK, указываем, что надо вернуть объект intent в качестве результата и закрываем Activity. Для значения цветов используем системные константы.
Аналогично создаем Activity для выбора выравнивания.
Здесь все аналогично, как и в ColorActivity. Только работаем не с цветами, а с выравниванием. Не забудьте прописать оба Activity в манифесте.
Теперь можем завершить код в MainActivity.java. Добавим пару своих констант в класс для удобства:
Эти константы далее будем использовать в качестве requestCode.
Допишем метод onClick:
Мы определяем, какая кнопка была нажата и посылаем Intent с ожиданием возврата результата. Два вызова отличаются классом вызываемого Activity и параметром requestCode в методе startActivityForResult. При вызове ColorActivity используем константу REQUEST_CODE_COLOR, а при вызове AlignActivity — REQUEST_CODE_ALIGN. Эту константу мы обратно получим в методе обработки результата – onActivityResult, и по ней сможем определить из какого именно Activity пришел результат.
Давайте реализуем метод onActivityResult в MainActivity.java:
Для наглядности пишем в лог значения переменных.
Вспоминаем, что в ColorActivity и AlignActivity в методе setResult мы ставили статус RESULT_OK при отправке результата. Значит в onActivityResult нам надо ожидать этот статус, как обозначение успешного окончания вызова.
Если вызов прошел успешно (resultCode = RESULT_OK), то мы смотрим значение requestCode. Если оно равно константе REQUEST_CODE_COLOR, то вспоминаем, что мы использовали эту константу в методе startActivityForResult, когда отправляли запрос на выбор цвета. Значит, нам пришел результат этого выбора. Мы берем Intent (data) и извлекаем из него значение объекта с именем color и присваиваем это значение цвету текста в TextView. Константа Color.WHITE в методе getIntExtra означает значение по умолчанию. Т.е. если в Intent не найдется объекта с именем color, то метод вернет белый (white) цвет.
Аналогично для REQUEST_CODE_ALIGN. Эту константу мы использовали для запроса выбора выравнивания. И если в методе onActivityResult параметр requestCode = этой константе, значит пришел ответ на запрос выравнивания. И мы считываем это значение из Intent и присваиваем его атрибуту Gravity для TextView.
Если resultCode не равен RESULT_OK, значит что-то пошло не так. Выводим на экран соответствующее сообщение. Этот случай может наступить, например, если на экране выбора не делать выбор, а нажать кнопку Назад.
Давайте все сохраним и запустим приложение.
и выберем, например Red
requestCode = 1, resultCode = -1
requestCode пришедший в метод onActivityResult равен 1. Все верно, это значение константы REQUEST_CODE_COLOR, которое мы использовали при вызове.
resultCode = -1 – это значение системной константы RESULT_OK
Т.е. все верно, пришел ответ на запрос цвета, и его статус = RESULT_OK.
Теперь жмем Alignment и выбираем Right, получаем выравнивание вправо:
requestCode = 2, resultCode = -1
requestCode = 2, что равно константе REQUEST_CODE_ALIGN. Значит пришел ответ на запрос выравнивания.
resultCode = -1, т.е. RESULT_OK.
Теперь снова жмем Color
но вместо того, чтобы выбрать цвет нажмем кнопку назад
Отобразилось наше сообщение об ошибке. Смотрим логи:
requestCode = 1, resultCode = 0
requestCode = 1 – все верно, мы запрашивали цвет (REQUEST_CODE_COLOR)
resultCode = 0, это значение константы RESULT_CANCELED, значит вызов прошел неудачно
Ограничений на значение статуса в методе setResult нет. RESULT_OK и RESULT_CANCELED – системные общепринятые константы. Но вы можете свободно использовать свои значения, если в этом есть необходимость.
Итак, подведем итог.
requestCode – это в некотором роде ID запроса. Задается в методе startActivityForResult, и проверяется потом в onActivityResult, чтобы точно знать, на какой вызов пришел ответ.
resultCode – статус вызова. Задается в методе setResult, и проверяется в onActivityResult, чтобы понять насколько успешно прошел вызов. Если при вызове что-то пошло не так, то вернется системная константа RESULT_CANCELED.
Полный код MainActivity.java:
На следующем уроке:
— узнаем что такое URI
— вызываем системные приложения (браузер, звонилка, карта)
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме
Источник