Сегодня практически каждый сталкивается с ОС Android, даже заядлые поклонники системы iOS наверняка смотрели на нее. Операционная система Android была разработана для устройств с сенсорным экраном, таких как планшеты и смартфоны. Он основан на ядре Linux от Google и сегодня является одной из самых популярных мобильных операционных систем.
Разработчику
Как правильно собрать Flow из UI-компонентов
A safer way to collect flows from Android UIs — статья о том, как написать с использованием Kotlin Flow асинхронный код, который не будет страдать от проблем перерасхода ресурсов.
По мнению разработчиков из Google, современный подход к написанию программ для Android выглядит следующим образом: уровень бизнес-логики предоставляет функции приостановки и производителей Flow, а компоненты пользовательского интерфейса вызывают функции приостановки или подписываются на Flow и обновляют пользовательский интерфейс в соответствии с входящими данными.
Выглядеть это все может примерно так. Функция — продюсер обновлений местоположения:
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 Producer продолжит отправлять данные обновления местоположения.
В этом случае, чтобы избежать этой проблемы, вы можете запускать и останавливать корутину , обработчик 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.
, который можно применять напрямую:
locationProvider.locationFlow()
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.onEach {
// Новое местоположение — обновляем UI
}
.launchIn(lifecycleScope)
По сути, все эти API — это более современная и гибкая замена LiveData.
Способы очистить код
Noisy Code With Kotlin Scopes — хорошая статья о том, как сделать код на Kotlin чище, понятнее и однозначнее.
Проблема номер 1: let. Многие программисты привыкли использовать let
в качестве простой и удобной альтернативы if (
:
fun deleteImage(){
var imageFile : File ? = ...
imageFile?.let {
if(it.exists()) it.delete()
}
}
Нет смысла это делать. Это плохой способ использовать it, потому что многие из них могут смешаться, если в вашем коде появится другая похожая лямбда. Можете попробовать исправить это с помощью файла imageFile?.let { image ->
, но вы в конечном итоге сделаете еще хуже, так как в той же области появится другая переменная, которая ссылается на то же значение, но с другим именем. И это имя придется придумать!
На самом деле в большинстве случаев исправить эту проблему можно простым отказом от let
:
С этим кодом все в порядке. Умное приведение типов сделает свою работу, и ты сможешь ссылаться на 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
Все о PendingIntents — статья, объясняющая, что такое PendingIntent и зачем он вам нужен. Будет полезно в связи с изменениями в обработке PendingIntent в Android 12.
Проще говоря, PendingIntent — это объект-оболочка для 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
)
Топ-10 лайфхаков
Ten #AndroidLifeHacks You Can Use Today — сборник маленьких полезных функций для Android-разработчиков. Наиболее интересные:
-
Функция fadeTo. Вместо
View.
лучше применять функцию, которая не просто мгновенно скроет view с экрана, а сделает это с анимацией. Функция fadeTo позволяет сделать это идемпотентно (можно вызывать сколько угодно раз подряд, не сбрасывая анимацию), с указанием продолжительности и конечной прозрачности.setVisibility( ) -
Функция 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.
. Чтобы облегчить себе жизнь, можно применить такой делегат для более удобной работы с Timber:tag( TAG). i( …) 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)
Библиотеки
- Maps Android KTX — Kotlin-расширения для Google Maps SDK;
- LabeledSeekSlider — ползунок с индикатором;
- Holi — библиотека цветов и градиентов;
- TestParameterInjector — тест‑раннер для Junit4 для запуска параметризованных тестов;
- EitherNet — библиотека для обертки ответов Retrofit (позволяет получать ошибки как возвращаемое значение вместо исключений);
- Bindables — библиотека для уведомления UI-слоя об изменениях в моделях;
- Lazybones — библиотека для управления жизненным циклом объектов в зависимости от жизненного цикла активности/приложения;
- Parsus — фреймворк для создания парсеров;
- RoundedProgressBar — очередной прогрессбар.
Итоги
Аппаратные средства Android имеют встроенную защиту. В безопасной среде исполнения Trusted Executions Environment (TEE) запускаются все функции безопасности, такие как блокировка экрана или шифрование данных. В то же время, приложения работают в изолированной программно-аппаратной среде, которая ограничивает доступ другому ПО. Другими словами, у вас есть всё для безопасной и продуктивной работы.