Решаем стандартные задачи с Result API на примере смены аватарки

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привет, меня зовут Александр, я Android-разработчик в Amazing Apps (мы делаем мобильные приложения в категории Health & Fitness, которые скачали уже более 100 млн человек). Давайте сегодня поговорим о Result API. Посмотрим на дополнительные возможности этого инструмента, которые идут из коробки, но часто остаются незамеченными.

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 и парсинг данных.

Вот так выглядит реализованный контракт, который на вход принимает значения barcore и возвращает название продукта:

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::photoUri.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 в отдельный класс.

Уверен, вы найдёте немало других дополнительных возможностей от использования Result API — рассказывайте о таких, буду рад послушать!

👍НравитсяПонравилось13
В избранноеВ избранном1
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
но в нашем коде ещё некоторое время с момента перехода можно было встретить фрагменты с использованием onActivityResult

Еще долго будете встречать. Пока внешие зависимости не перейдут на Result API.

Да, есть такая проблема. Учитывая что даже не все библиотеки от google перешли)

Подписаться на комментарии