Решаем стандартные задачи с Result API на примере смены аватарки
Привет, меня зовут Александр, я 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 — рассказывайте о таких, буду рад послушать!
2 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарівЕще долго будете встречать. Пока внешие зависимости не перейдут на Result API.
Да, есть такая проблема. Учитывая что даже не все библиотеки от google перешли)