Некоторые советы и лайфхаки по ОС Android

Некоторые советы и лайфхаки по ОС Android

Сегодня практически каждый сталкивается с ОС 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.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потому что многие из них могут смешатьсяесли в вашем коде появится другая похожая лямбдаМожете попробовать исправить это с помощью файла 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

Все о 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.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)
 

Библиотеки

  • Maps Android KTX — Kotlin-рас­ширения для Google Maps SDK;
  • LabeledSeekSlider — пол­зунок с инди­като­ром;
  • Holi — биб­лиоте­ка цве­тов и гра­диен­тов;
  • TestParameterInjector — тест‑ран­нер для Junit4 для запус­ка парамет­ризован­ных тес­тов;
  • EitherNet — биб­лиоте­ка для обер­тки отве­тов Retrofit (поз­воля­ет получать ошиб­ки как воз­вра­щаемое зна­чение вмес­то исклю­чений);
  • Bindables — биб­лиоте­ка для уве­дом­ления UI-слоя об изме­нени­ях в моделях;
  • Lazybones — биб­лиоте­ка для управле­ния жиз­ненным цик­лом объ­ектов в зависи­мос­ти от жиз­ненно­го цик­ла активнос­ти/при­ложе­ния;
  • Parsus — фрей­мворк для соз­дания пар­серов;
  • RoundedProgressBar — оче­ред­ной прог­рес­сбар.

Итоги

Аппаратные средства Android имеют встроенную защиту. В безопасной среде исполнения Trusted Executions Environment (TEE) запускаются все функции безопасности, такие как блокировка экрана или шифрование данных. В то же время, приложения работают в изолированной программно-аппаратной среде, которая ограничивает доступ другому ПО. Другими словами, у вас есть всё для безопасной и продуктивной работы.

 

Click to rate this post!
[Total: 0 Average: 0]

Leave a reply:

Your email address will not be published.