Хакер - Android: советы и лайфхаки
hacker_frei
Евгений Зобнин
Содержание статьи
- Разработчику
- Правильный способ сбора Flow из UI-компонентов
- Как сделать код чище
- Что такое PendingIntent
- Десять полезных лайфхаков
- Библиотеки
Сегодня в выпуске: правильный способ сбора Flow из UI-компонентов, советы, как сделать код на Kotlin чище, десять полезных лайфхаков и сниппетов кода на все случаи жизни. А также объяснение сути PendingIntent и подборка библиотек для разработчиков.
РАЗРАБОТЧИКУ
Правильный способ сбора Flow из UI-компонентов
A safer way to collect flows from Android UIs — статья о том, как написать с использованием Kotlin Flow асинхронный код, который не будет страдать от проблем перерасхода ресурсов.
Современный подход к написанию приложений для Android выглядит примерно так: слой бизнес‑логики выставляет наружу suspend-функции и продюсеры Flow, а UI-компоненты вызывают suspend-функции или подписываются на Flow и обновляют UI в соответствии с пришедшими данными.
Выглядеть это все может примерно так. Функция — продюсер обновлений местоположения:
fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
try { offer(result.lastLocation) } catch(e: Exception) {}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
.addOnFailureListener { e ->
close(e) // In case of exception, close the Flow
}
awaitClose {
removeLocationUpdates(callback)
}
}
Часть UI-компонента, подписывающаяся на Flow:
lifecycleScope.launchWhenStarted {
locationProvider.locationFlow().collect {
// Новое местоположение — обновляем UI
}
}
На первый взгляд — все хорошо. Благодаря использованию lifecycleScope мы научили код реагировать на изменение жизненного цикла приложения — при уходе приложения в фон обработка значений местоположения будет приостановлена. Но! Продюсер Flow продолжит отправлять данные об обновлении местоположения.
Чтобы избежать такой проблемы, можно либо самостоятельно запускать и останавливать корутину — обработчик Flow при изменении жизненного цикла приложения, либо использовать lifecycleOwner.addRepeatingJob из библиотеки lifecycle-runtime-ktx версии 2.4.0-alpha01.
lifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
locationProvider.locationFlow().collect {
// Новое местоположение — обновляем UI
}
}
Выглядит почти так же, как предыдущий пример. Однако в данном случае корутина будет полностью остановлена при переходе приложения в любое состояние, отличное от Lifecycle.State.STARTED, и запущена снова при переходе в это состояние. Вместе с ней будет остановлен и продюсер данных о местоположении.
Того же эффекта можно добиться, используя suspend-функцию repeatOnLifecycle:
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
locationProvider.locationFlow().collect {
// Новое местоположение — обновляем UI
}
}
}
Она удобна в тех случаях, когда перед сбором данных необходимо выполнить определенную работу внутри suspend-функции.
Внутри эта функция использует оператор Flow.flowWithLifecycle, который можно применять напрямую:
locationProvider.locationFlow()
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.onEach {
// Новое местоположение — обновляем UI
}
.launchIn(lifecycleScope)
По сути, все эти API — это более современная и гибкая замена LiveData.
Как сделать код чище
Noisy Code With Kotlin Scopes — хорошая статья о том, как сделать код на Kotlin чище, понятнее и однозначнее.
Проблема номер 1: let. Многие программисты привыкли использовать let в качестве простой и удобной альтернативы if (x == null):
fun deleteImage(){
var imageFile : File ? = ...
imageFile?.let {
if(it.exists()) it.delete()
}
}
Так делать не стоит. Использование it — дурной тон, потому что множественные it могут смешаться, если в коде появится еще одна подобная лямбда. Ты можешь попробовать исправить это с помощью конструкции imageFile?.let { image ->, но в итоге сделаешь еще хуже, потому что в той же области видимости появится еще одна переменная, которая ссылается на то же значение, но имеет другое имя. И это имя надо будет придумать!
На самом деле в большинстве случаев исправить эту проблему можно простым отказом от let:
fun deleteImage(){
val imageFile : File ? = ...
if(imageFile != null) {
if(imageFile.exists()) imageFile.delete()
}
}
С этим кодом все в порядке. Умное приведение типов сделает свою работу, и ты сможешь ссылаться на imageFile после проверки как на не nullable-переменную.
Но! Такой прием не сработает, если речь идет не о локальной переменной, а о полях класса. Из‑за того что поля класса могут изменяться несколькими методами, работающими в разных потоках, компилятор не сможет использовать смарткастинг.
Как раз здесь и можно использовать let.
Но есть и более необычные способы. Например, выходить из функции, если значение поля null:
fun deleteImage() {
val imageFile = getImage() ?: return
...
}
Или использовать метод takeIf:
fun deleteImage() {
getImage()?.takeIf { it.exists }?.let {it.delete()}
}
Можно даже скомбинировать оба подхода:
fun deleteImage() {
val image = getImage()?.takeIf { it.exists } ?: return
image.delete()
}
Проблема номер 2: also и apply. Синтаксис Kotlin поощряет использование лямбд везде, где только возможно. Иногда это приводит к созданию весьма монструозных конструкций:
Intent(context, MyActivity::class.java).apply {
putExtra("data", 123)
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
}).also { intent ->
startActivity(this@FooActivity, intent)
}
Смысл такого кода в том, чтобы ограничить создание интента и его использование двумя различными областями видимости, что в теории должно благотворно повлиять на его модульность.
На самом деле такие конструкции только захламляют код. Гораздо красивее выглядит его более прямолинейная версия:
val intent = Intent(context, MyActivity::class.java).apply {
putExtra("data", 123)
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
})
startActivity(this@FooActivity,intent)
А еще лучше вынести код конфигурирования объекта в отдельную функцию:
fun startSomeActivity() {
startActivity(getSomeIntent())
}
fun getSomeIntent() = Intent(context, SomeActivity::class.java).apply {
// ...
}
Проблема номер 3: run. Распространенный прием — использовать функцию run для обрамления блоков кода:
val userName: String
get() {
preferenceManager.getInstance()?.run {
getName()
} ?: run {
getString(R.string.stranger)
}
}
Это абсолютно бессмысленное захламление кода. Оно затрудняет чтение простого по своей сути кода:
val userName: String
get() = preferenceManager.getInstance()?.getName() ?: getString(R.string.stranger)
Если же кода в блоке «если null» больше одной строчки, то можно использовать такую конструкцию:
preferenceManager.getInstance()?.getName().orDefault {
Log.w(“Name not found, returning default”)
getString(R.string.stranger)
}
Проблема номер 4: with. В данном случае проблема не в самой функции with, а в ее игнорировании. Разработчики просто не используют эту функцию, несмотря на всю ее красоту:
val binding = MainLayoutBinding.inflate(layoutInflater)
with(binding) {
textName.text = "ch8n"
textTwitter.text = "twitter@ch8n2"
})
Причин не использовать ее обычно две:
- ее нельзя использовать в цепочках вызовов функций;
- она плохо дружит с nullable-переменными.
Что такое PendingIntent
All About PendingIntents — статья, объясняющая, что такое PendingIntent и зачем он нужен. Будет полезна в связи с изменениями в обработке PendingIntent в Android 12.
Коротко говоря, PendingIntent — это объект — обертка для Intent, который позволяет передать Intent другому приложению с целью его выполнения в будущем от имени создавшего Intent приложения. PendingIntent, в частности, используется, чтобы указать системе, что нужно сделать при нажатии на уведомление. В этом случае система запускает интент, указанный в PendingIntent, так, как будто бы его запустило создавшее уведомление приложение.
Простейший пример:
val intent = Intent(applicationContext, MainActivity::class.java).apply {
action = NOTIFICATION_ACTION
data = deepLink
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,
NOTIFICATION_REQUEST_CODE,
intent,
PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(
applicationContext,
NOTIFICATION_CHANNEL
).apply {
// ...
setContentIntent(pendingIntent)
// ...
}.build()
notificationManager.notify(
NOTIFICATION_TAG,
NOTIFICATION_ID,
notification
)
Сначала мы создаем Intent, а затем заворачиваем его в PendingIntent. С помощью флага PendingIntent.FLAG_IMMUTABLE мы указываем, что не хотим, чтобы система или кто‑либо еще, получивший доступ к этому PendingIntent, мог его изменить. Такой флаг (или флаг PendingIntent.FLAG_MUTABLE) обязательно указывать в Android 12.
При обновлении уведомления мы должны будем указать дополнительный флаг, чтобы заменить старый PendingIntent новым:
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
Зачем же может понадобиться изменение интента внутри PendingIntent? Например, для создания системы плагинов, когда плагин передает приложению‑хосту PendingIntent и тот вносит в него изменения в соответствии с вводом пользователя.
Для изменения PendingIntent используется достаточно странный способ, когда методу send передается еще один интент, атрибуты которого становятся частью атрибутов оригинального интента:
val intentWithExtrasToFill = Intent().apply {
putExtra(EXTRA_CUSTOMER_MESSAGE, customerMessage)
}
mutablePendingIntent.send(
applicationContext,
PENDING_INTENT_CODE,
intentWithExtrasToFill
)
Десять полезных лайфхаков
Ten #AndroidLifeHacks You Can Use Today — сборник маленьких полезных функций для Android-разработчиков. Наиболее интересные:
- Функция fadeTo. Вместо
View.setVisibility()лучше применять функцию, которая не просто мгновенно скроет view с экрана, а сделает это с анимацией. Функция fadeTo позволяет сделать это идемпотентно (можно вызывать сколько угодно раз подряд, не сбрасывая анимацию), с указанием продолжительности и конечной прозрачности. - Функция mapDistinct. Простейшая функция — расширение Flow, которая сначала вызывает
map, а затемdistinctUntilChanged()(пропускать одинаковые значения). Код функции представляет собой одну строчку: fun <T, V> Flow<T>.mapDistinct(mapper: suspend (T) -> V): Flow<V> = map(mapper).distinctUntilChanged()- Упрощенный Delegates.observable. Функция‑делегат
observableможет быть очень удобной, когда необходимо следить за изменениями значения переменной. Но чаще требуется улавливать не все изменения, а только новые, изменившиеся. В этом случае подойдет функция uniqueObservable. - Перезапускаемая Job. При разработке приложений на Kotlin часто требуется завершить предыдущую корутину перед запуском следующей (например, при выполнении поиска или обновлении экрана). Упростить эту процедуру можно с помощью ConflatedJob, которая при запуске завершает предыдущую корутину.
- Более удобный Timber. Многие разработчики используют Timber для логирования. Однако делать это напрямую не совсем удобно, приходится писать длинные строки вроде
Timber.tag(TAG).i(…). Чтобы облегчить себе жизнь, можно применить такой делегат для более удобной работы с Timber: class MyClass {// This will automatically have the TAG "MyClass"private val log by timber()fun logSomething() {log.i("Hello")log.w(Exception(), "World")}}- Number.dp. При разработке часто требуется преобразовать единицы DP (Density-independent Pixels) в пиксели. Для этого можно использовать такую функцию‑расширение:
val Number.dp get() = toFloat() * (Resources.getSystem().displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei