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

Kate

Administrator
Команда форума
Привет, меня зовут Александр, я Android-разработчик в Amazing Apps. Давайте сегодня поговорим о 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 в отдельный класс.

 
Сверху