Привет, меня зовут Александр, я Android-разработчик в Amazing Apps. Давайте сегодня поговорим о Result API. Посмотрим на дополнительные возможности этого инструмента, которые идут из коробки, но часто остаются незамеченными.
Result API де-факто стал стандартом передачи данных между активностями в Android-приложениях, вместо onActivityResult, который сейчас существует в статусе Deprecated. Переход на новый API происходит довольно несложно (спасибо Google), но всё равно требует времени, случается не одномоментно.
Так мы в команде перешли на новое API в 2020 году, начиная с альфа-версии, но в нашем коде ещё некоторое время с момента перехода можно было встретить фрагменты с использованием onActivityResult. Эта статья — об одном из таких фрагментов, но не в момент перехода на новую реализацию, а чуть позже. Использование Result API помогло вынести логику из фрагмента в отдельный класс — довольно удобно для решения стандартных задач при создании Android-приложения.
Давайте разбираться на примере.
Ключевая сущность при работе с Result API — контракт. Это класс, который реализует интерфейс ActivityResultContract<I,O>, где I определяет тип входных данных, необходимых для запуска Activity; а O — тип возвращаемого результата.
Для подключения последней стабильной версии на момент написания статьи нужно отредактировать build.gradle:
implementation 'androidx.activity:activity:1.3.1'
implementation 'androidx.fragment:fragment:1.3.6'
Для KTX-версии:
implementation 'androidx.activity:activity-ktx:1.3.1'
implementation 'androidx.fragment:fragment-ktx:1.3.6'
Result API позволяет переиспользовать один и тот же код в разных частях приложения (привет Дяде Бобу и всем, кто читал «Чистый код»).
Но самые хорошие новости на мой взгляд в том, что для множества стандартных задач уже существуют контракты «из коробки». Перечень всех стандартных контрактов можно найти по ссылке.
По факту значительная часть работы с Result API сводится к регистрации предсозданного контракта, а затем его запуска.
getContentLauncher =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
//Работа с полученным URI
}
getContentLauncher.launch("image/*")
Создавать собственные контракты — можно. Для этого нужно реализовать интерфейс ActivityResultContract<I,O> и два метода:
class BarcodeRecognitionContract : ActivityResultContract<Long, String>() {
override fun createIntent(context: Context, input: Long?): Intent {
return Intent(context, BarcodeActivity::class.java)
.putExtra("extra_barcode", input)
}
override fun parseResult(resultCode: Int, intent: Intent?): String? = when {
resultCode != Activity.RESULT_OK -> null
else -> intent?.getStringExtra("extra_product_name")
}
}
Использование этого контракта никак не будет отличаться от работы с любым стандартным контрактом «из коробки».
Давайте теперь перейдём к примеру.
Это приложение использует CameraX: <uses-permission android:name="android.permission.CAMERA"/>. Нам нужно получить доступ к камере даже при использовании Intent. Не самый очевидный подход к работе с картинками, в Google Issue Tracker даже заведён баг на этот счёт. Но такие уж вводные условия.
Дополнительная возможность, которую даёт Result API для решения этой задачи — возможность вынести логику запроса и получение картинки за пределы активности или фрагмента. Это позволит нам более гибко и удобно переиспользовать написанный код в разных местах приложения.
Вынесение логики смены картинки в отдельный класс — сделаем с помощью передачи ActivityResultRegistry в качестве параметра.
Давайте посмотрим код:
class PhotoPicker(
activityResultRegistry: ActivityResultRegistry,
private val application: Application,
private val callback: (image: Uri?) -> Unit
) {
private lateinit var photoUri: Uri
//Запрашиваем картинку на устройстве
private val getContentLauncher = activityResultRegistry.register(
REGISTRY_KEY_GET_CONTENT,
ActivityResultContracts.GetContent()
) { uri -> callback.invoke(uri) }
//Вызываем камеру
private val takePhotoLauncher = activityResultRegistry.register(
REGISTRY_KEY_TAKE_PHOTO,
ActivityResultContracts.TakePicture()
) { if (result && this:hotoUri.isInitialized) callback.invoke(photoUri)
}
//Запрашиваем доступ к камере
private val requestPermissionLauncher = activityResultRegistry.register(
REGISTRY_KEY_PERMISSION,
ActivityResultContracts.RequestPermission()
) { result ->
if (result) {
photoUri = getTmpFileUri()
takePhotoLauncher.launch(photoUri)
}
}
fun pickPhoto() { getContentLauncher.launch("image/*") }
fun takePhoto() { requestPermissionLauncher.launch(Manifest.permission.CAMERA)}
private fun getTmpFileUri(): Uri {
val tmpFile = File("${application.cacheDir.absolutePath}${File.separator}tmp_image.jpg")
return FileProvider.getUriForFile(application, "${BuildConfig.APPLICATION_ID}.provider", tmpFile)
}
}
Теперь нам нужно создать и вызвать PhotoPicker:
class ProfileFragment : Fragment() {
private lateinit var photoPicker: PhotoPicker
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
photoPicker =
PhotoPicker(requireContext(), requireActivity().activityResultRegistry) { uri ->
if (uri == null) return@PhotoPicker
FileUtils.getBitmapFromUri(requireActivity().contentResolver,uri)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.btnTakePhoto.setOnClickListener {
photoPicker.takePhoto()
}
binding.btnPickPhoto.setOnClickListener {
photoPicker.pickPhoto()
}
}
}
Готово! Наш UI избавляется от ненужной логики — это даёт нам меньше копирования кода.
object ImageUtils {
fun getBitmapFromUri(contentResolver: ContentResolver, uri: Uri): Bitmap? {
val degrees = getRotationDegrees(contentResolver, uri)
//Используем use для автоматического закрытия InputStream после выполненного действия
return (contentResolver.openInputStream(uri))?.use {
BitmapFactory.decodeStream(it)?.rotate(degrees)
}
}
private fun getRotationDegrees(contentResolver: ContentResolver, imageUri: Uri): Float {
(contentResolver.openInputStream(imageUri) ?: return 0F).use {
val orientation = ExifInterface(it).getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL
)
return when (orientation) {
ExifInterface.ORIENTATION_ROTATE_270 -> 270F
ExifInterface.ORIENTATION_ROTATE_180 -> 180F
ExifInterface.ORIENTATION_ROTATE_90 -> 90F
else -> 0F
}
}
}
}
fun Bitmap.rotate(degrees: Float): Bitmap {
if (degrees != 0F) {
val matrix = Matrix()
matrix.postRotate(degrees)
return Bitmap.createBitmap(this, 0, 0, this.width, this.height, matrix, true)
}
return this
}
Надеюсь, этот код поможет вам избежать перевернутых картинок и работать с удобным для вас форматом изображений.
В этой статье мы поговорили об одной из таких возможностей: вынесении логики из Activity и Fragments в отдельный класс.
Result API де-факто стал стандартом передачи данных между активностями в Android-приложениях, вместо onActivityResult, который сейчас существует в статусе Deprecated. Переход на новый API происходит довольно несложно (спасибо Google), но всё равно требует времени, случается не одномоментно.
Так мы в команде перешли на новое API в 2020 году, начиная с альфа-версии, но в нашем коде ещё некоторое время с момента перехода можно было встретить фрагменты с использованием onActivityResult. Эта статья — об одном из таких фрагментов, но не в момент перехода на новую реализацию, а чуть позже. Использование Result API помогло вынести логику из фрагмента в отдельный класс — довольно удобно для решения стандартных задач при создании Android-приложения.
Давайте разбираться на примере.
Базовые факты о Result API
Для начала давайте вспомним вводное об Result API. Новое API появилось в 2020 году стараниями Google; доступно начиная с AndroidX Activity 1.2.0-alpha02 и Fragment 1.3.0-alpha02.Ключевая сущность при работе с Result API — контракт. Это класс, который реализует интерфейс ActivityResultContract<I,O>, где I определяет тип входных данных, необходимых для запуска Activity; а O — тип возвращаемого результата.
Для подключения последней стабильной версии на момент написания статьи нужно отредактировать build.gradle:
implementation 'androidx.activity:activity:1.3.1'
implementation 'androidx.fragment:fragment:1.3.6'
Для KTX-версии:
implementation 'androidx.activity:activity-ktx:1.3.1'
implementation 'androidx.fragment:fragment-ktx:1.3.6'
Result API позволяет переиспользовать один и тот же код в разных частях приложения (привет Дяде Бобу и всем, кто читал «Чистый код»).
Но самые хорошие новости на мой взгляд в том, что для множества стандартных задач уже существуют контракты «из коробки». Перечень всех стандартных контрактов можно найти по ссылке.
По факту значительная часть работы с Result API сводится к регистрации предсозданного контракта, а затем его запуска.
getContentLauncher =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
//Работа с полученным URI
}
getContentLauncher.launch("image/*")
Создавать собственные контракты — можно. Для этого нужно реализовать интерфейс ActivityResultContract<I,O> и два метода:
- createIntent() — принимает входные данные и создает Intent, который будет в дальнейшем запущен вызовом launch();
- parseResult() — отвечает за возврат результата, обработку resultCode и парсинг данных.
class BarcodeRecognitionContract : ActivityResultContract<Long, String>() {
override fun createIntent(context: Context, input: Long?): Intent {
return Intent(context, BarcodeActivity::class.java)
.putExtra("extra_barcode", input)
}
override fun parseResult(resultCode: Int, intent: Intent?): String? = when {
resultCode != Activity.RESULT_OK -> null
else -> intent?.getStringExtra("extra_product_name")
}
}
Использование этого контракта никак не будет отличаться от работы с любым стандартным контрактом «из коробки».
Давайте теперь перейдём к примеру.
Меняем фото пользователя с помощью Result API
Приложение, о котором пойдёт речь ниже, относится к категории Health & Fitness. Для примера нам понадобится функциональность не всего приложения, но стандартная задача — смена фото пользователя.Это приложение использует CameraX: <uses-permission android:name="android.permission.CAMERA"/>. Нам нужно получить доступ к камере даже при использовании Intent. Не самый очевидный подход к работе с картинками, в Google Issue Tracker даже заведён баг на этот счёт. Но такие уж вводные условия.
Дополнительная возможность, которую даёт Result API для решения этой задачи — возможность вынести логику запроса и получение картинки за пределы активности или фрагмента. Это позволит нам более гибко и удобно переиспользовать написанный код в разных местах приложения.
Вынесение логики смены картинки в отдельный класс — сделаем с помощью передачи ActivityResultRegistry в качестве параметра.
Давайте посмотрим код:
class PhotoPicker(
activityResultRegistry: ActivityResultRegistry,
private val application: Application,
private val callback: (image: Uri?) -> Unit
) {
private lateinit var photoUri: Uri
//Запрашиваем картинку на устройстве
private val getContentLauncher = activityResultRegistry.register(
REGISTRY_KEY_GET_CONTENT,
ActivityResultContracts.GetContent()
) { uri -> callback.invoke(uri) }
//Вызываем камеру
private val takePhotoLauncher = activityResultRegistry.register(
REGISTRY_KEY_TAKE_PHOTO,
ActivityResultContracts.TakePicture()
) { if (result && this:hotoUri.isInitialized) callback.invoke(photoUri)
}
//Запрашиваем доступ к камере
private val requestPermissionLauncher = activityResultRegistry.register(
REGISTRY_KEY_PERMISSION,
ActivityResultContracts.RequestPermission()
) { result ->
if (result) {
photoUri = getTmpFileUri()
takePhotoLauncher.launch(photoUri)
}
}
fun pickPhoto() { getContentLauncher.launch("image/*") }
fun takePhoto() { requestPermissionLauncher.launch(Manifest.permission.CAMERA)}
private fun getTmpFileUri(): Uri {
val tmpFile = File("${application.cacheDir.absolutePath}${File.separator}tmp_image.jpg")
return FileProvider.getUriForFile(application, "${BuildConfig.APPLICATION_ID}.provider", tmpFile)
}
}
Теперь нам нужно создать и вызвать PhotoPicker:
class ProfileFragment : Fragment() {
private lateinit var photoPicker: PhotoPicker
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
photoPicker =
PhotoPicker(requireContext(), requireActivity().activityResultRegistry) { uri ->
if (uri == null) return@PhotoPicker
FileUtils.getBitmapFromUri(requireActivity().contentResolver,uri)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.btnTakePhoto.setOnClickListener {
photoPicker.takePhoto()
}
binding.btnPickPhoto.setOnClickListener {
photoPicker.pickPhoto()
}
}
}
Готово! Наш UI избавляется от ненужной логики — это даёт нам меньше копирования кода.
Бонус: ориентация фотографий и Bitmap
Раз зашла речь о работе с фотографиями, хочу поделиться кодом для нормализации ориентации фотографий и преобразования Uri в Bitmap.object ImageUtils {
fun getBitmapFromUri(contentResolver: ContentResolver, uri: Uri): Bitmap? {
val degrees = getRotationDegrees(contentResolver, uri)
//Используем use для автоматического закрытия InputStream после выполненного действия
return (contentResolver.openInputStream(uri))?.use {
BitmapFactory.decodeStream(it)?.rotate(degrees)
}
}
private fun getRotationDegrees(contentResolver: ContentResolver, imageUri: Uri): Float {
(contentResolver.openInputStream(imageUri) ?: return 0F).use {
val orientation = ExifInterface(it).getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL
)
return when (orientation) {
ExifInterface.ORIENTATION_ROTATE_270 -> 270F
ExifInterface.ORIENTATION_ROTATE_180 -> 180F
ExifInterface.ORIENTATION_ROTATE_90 -> 90F
else -> 0F
}
}
}
}
fun Bitmap.rotate(degrees: Float): Bitmap {
if (degrees != 0F) {
val matrix = Matrix()
matrix.postRotate(degrees)
return Bitmap.createBitmap(this, 0, 0, this.width, this.height, matrix, true)
}
return this
}
Надеюсь, этот код поможет вам избежать перевернутых картинок и работать с удобным для вас форматом изображений.
Ещё больше возможностей Result API
На мой взгляд Result API заметно упрощает жизнь Android-разработчика. «Регистрируй предсозданный контракт, запускай» — такой подход настолько удобен, что остальные возможности этого инструментария часто остаются незамеченными.В этой статье мы поговорили об одной из таких возможностей: вынесении логики из Activity и Fragments в отдельный класс.
DOU
DOU – Найбільша спільнота розробників України. Все про IT: цікаві статті, інтервʼю, розслідування, дослідження ринку, свіжі новини та події. Спілкування на форумі з айтівцями на найгарячіші теми та технічні матеріали від експертів. Вакансії, рейтинг IT-компаній, відгуки співробітників, аналітика...
dou.ua