- Получаем результат правильно (Часть 1). Activity Result API
- Чем плох onActivityResult()?
- Используем Activity Result API
- Шаг 1. Создание контракта
- Шаг 2. Регистрация контракта
- Шаг 3. Запуск контракта
- Важно!
- Работа с runtime permissions
- Подводим итоги
- AndroidX Activity Result APIs — Taking a Picture
- Adding the relevant dependencies
- Requesting a Permission
- Requesting Multiple Permissions
- Introducing the Activity Result APIs
- The basics
- Repetitive logic
- Tight coupling
- The Activity Result API
- The built in ActivityResultContracts
- The ActivityResultRegistry
- Gotchas
- Last thoughts
Получаем результат правильно (Часть 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 репозитории.
Источник
AndroidX Activity Result APIs — Taking a Picture
As of AndroidX Activity 1.2.0-alpha02 and Fragment 1.3.0-alpha02, a new mechanism for requesting results from an activity or fragment is available to Android developers.
Instead of calling startActivityForResult() with a request code and listening for the potential result in onActivityResult(), we now simply use a contract which defines our input(s) and output(s) and how we want to handle the outputs when they come.
We can define our own contracts but thankfully some common scenarios are catered for ‘out of the box’. In this article I wanted to cover the common task of allowing your users to take an image with their camera.
Adding the relevant dependencies
In order to take advantage of the result APIs you need to add the right dependencies as follows:
You can find release notes and latest versions of these at the Android developer site:
Requesting a Permission
As always, we can (and should) check if a certain permission is currently allowed before attempting to carry out an action that requires it. In our case we want to access the camera and can check the permissions like so:
Assuming this permission isn’t granted, we can simply request it using the RequestPermission contract.
First, create an ActivityResultLauncher which we can use when we want to kick off our request for the permission:
Then simply launch the request with the required permission like so:
The RequestPermission contract we used above means that our launch method will take a single permission as its input. This will change depending on the contract you have decided to use as you will see below.
The callback you defined above will be invoked when the user allows/denies your permission request and you can handle their response appropriately.
Requesting Multiple Permissions
A more realistic scenario would involve requesting multiple permissions, as in the camera example above you are more likely to want to receive the picture the user took and do something with it.
The normal way to achieve this is to create a file and provide it to the camera app to store the image in, but this requires the ability to write to storage and can very easily be worked into the existing code above with some small changes.
Update the creation of your ActivityResultLauncher, changing:
We’re now using a different pre-defined contract which expects one or more permissions to request and will return a map of permission to boolean result instead of a single boolean.
Your callback will also need updated to accommodate this and should look something like this:
We can now tell whether the user has granted or denied each permission we requested individually.
This is now launched using something like this:
Note that as we are now using a different contract, our launch parameters have changed and the RequestMultiplePermissions contract expects an array of 1 or more permissions as input.
When the user has granted both of our permissions we can then create a new file to store any image that’s taken and launch our request for the user to take the picture.
Create a file by doing something similar to this, storing the current path for use later:
Get a content URI for your file to provide to the camera app using your file provider:
Create another ActivityResultLauncher using the pre-built “TakePicture” contract.
And finally, The TakePicture() contract expects a Uri as input so you launch your request, passing it the Uri of the file you created to store the image.
Источник
Introducing the Activity Result APIs
Google has finally released the new Activity Result APIs, which are something I’ve been looking forward to for a very long time. In this post I’ll run through the basics, how you can create your own contracts, and how this allows us to abstract away even more responsibilities from your UI.
The basics
As I’m sure you’re aware, when you want to request data from another activity (say for instance, requesting an image from a camera), you would override onActivityResult . This works absolutely fine but has a couple of downsides:
Repetitive logic
Actually receiving the result requires some slightly tedious code. You need to check the request code to see if it was your activity that requested what’s being returned, then you need to check to see whether or not the request was successful. After that, you pull the data out of the Intent object. It’s become commonplace to see code like this littered around the UI:
Wouldn’t it be nice if we could abstract this away?
Tight coupling
The only place to get these onActivityResult callbacks is in an Activity or Fragment and there’s simply no getting around that.
The Activity Result API
Starting with Activity 1.2.0-alpha02 and Fragment 1.3.0-alpha02 , we now have a nice abstraction which allows us to handle onActivityResult callbacks in a neat and reusable way, and Google were kind enough to add a few commonly used contracts so that we don’t need to manage them ourselves.
This is the new callback which we can register at any point in our Activity . Making the actual request is as simple as invoking takePicture :
So what’s going on here? Let’s break it down slightly. takePicture is just a callback which returns a nullable Bitmap — whether or not it’s null depends on whether or not the onActivityResult process was successful. prepareCall then registers this call into a new feature on ComponentActivity called the ActivityResultRegistry — we’ll come back to this later. ActivityResultContracts.TakePicture() is one of the built-in helpers which Google have created for us, and finally invoking takePicture actually triggers the Intent in the same way that you would previously with Activity.startActivityForResult(intent, REQUEST_CODE) .
The built in ActivityResultContracts
The built-in ActivityResultContracts currently include a few common operations:
- Requesting multiple permissions with RequestPermissions
- Requesting just one permission with RequestPermission
- Making a phone call with Dial
- And taking a picture with TakePicture
So what do these actually do under the hood? Let’s take a closer look at TakePicture :
ActivityResultContract takes two type parameters which correspond to data that’s required to make this request (in this case, nothing or Void but quite often a String such as a URI), and the data type to be returned — a Bitmap . As we can see, the contract has two functions, createIntent and parseResult :
In these built-in contracts, you can see we’ve moved the logic which previously tended to live in our Activity and moved them to pretty trivial helper classes. It’s also incredibly easy for us to write our own reusable contracts: we simply specify the inputs and outputs, and handle the Intent creation and Intent + resultCode parsing ourselves.
For a concrete example: we often launch one of our own Activities to send custom data types back. Here’s a ActivityResultContract which would allow us to handle that:
Lovely. However, this is still coupled to the Activity or Fragment . Is there a way around this?
The ActivityResultRegistry
Happily, there is, and it allows us to do all sorts of clever things. The ActivityResultRegistry is a new feature of the ComponentActivity class, and ultimately it contains a list of callbacks to be invoked when onActivityResult is triggered. All we require is a reference to one to be able to register our listeners from any class:
It’s worth pointing out that the resultLauncher() invocation here is an activity-ktx extension for invoking an ActivityResultLauncher of type Void , which is actually just:
Thanks to Ian Lake for letting me know about this one. The TakePictureHandler class allows our Activities to be more composable, and we can test this functionality fairly easily as a small, single-responsibility class, abstracted away from our view:
One slight downside of this is you have to remember to add the handler to the lifecycle observer in your Activity or Fragment . We can improve this a little so that it manages this task itself:
Here, we pass both an ActivityResultRegistry and a LifecycleOwner to the underlying implementation — this is because otherwise it wouldn’t be possible to test the implementation; we need to be able to pass our own result registry with a dispatching callback AND we need to be able to trigger the onCreate lifecycle callback. By keeping the TakePictureImpl internal , we can test this class thoroughly but expose a simpler API for the caller where we only pass the ComponentActivity . There’s a bit of Kotlin delegation magic here too, and if you’re unsure what this does you can check out one of my previous articles for some more info.
Back to the Activity Result API itself: there’s plenty of situations where this would be useful. We have a class which abstracts away Google’s in-app update API and emits a sealed class of results through a Kotlin Flow type. However the API requires implementing onActivityResult , and we haven’t been able to get around that, so we’ve had to pass the Intent returned through the callback manually to the class. Now we don’t have to do that, the class can be entirely self-contained. It’s a pretty complex example so I haven’t posted it here, but I might put it up separately once I’ve refactored it to use the new registry.
I’m also looking forward to seeing what abstractions people come up with for requesting permissions with this new API.
Gotchas
After some discussion, there does appear to be a small gotcha whilst using this API. Google are pretty explicit in their docs that you must re-register your callback in onCreate :
To clarify, these callbacks are not persisted on a configuration change — but any results returned are in-fact queued, so that when you re-register you’ll instantly receive the awaiting result. Basically to avoid weird behaviour, always make sure these callbacks are registered in onCreate ; don’t do anything weird like registering them ad-hoc in click listeners. Thanks to Gabor Varadi and Vasiliy Zukanov for spotting this and a good discussion on the underlying issue on Twitter. There’s also a great point made here:
The issue isn’t the number of pending requests, it’s that the ID for a result contract is dynamic based on an AtomicInteger.getAndIncrement instead of something stable. So if you misuse the API and call prepareCall each time you want to launch a result request, then you wouldn’t get the result if the camera low-memory-kills you. You only get it if you initialize the ResultCaller once in onCreate and re-use it each time.
So be aware of this behaviour, and that this may change in future. A bug has been filed with Google and this is an Alpha release, so perhaps this post will require updating in a few weeks.
Last thoughts
This is a useful abstraction and I’m super pleased that Google have released this. One minor thing to note is that the the current documentation on the activity result page appears to be incorrect in a couple of places (how to register a contract, how to dispatch a result in a test), but hopefully that’ll get resolved shortly.
Hopefully this has given you some good ideas for some neat abstractions and I’d love to see them, so please share anything you come up with. I’ll likely update this post in a few weeks with some bits and pieces that I come up with too.
Thanks for reading!
Edit: Thanks to Ian Lake for the tips on activity-ktx and pointing me in the direction of the documentation bug tracker.
Источник