Решаем стандартные задачи с 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 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів