Android: Проблема отрисовки в jetpack compose

35e3a0dfb1a01939a1b99dcf3432dadb.png

Привет всем! Хочу поделится одной интересной проблемой (и ее решением), с которой пришлось столкнуться при использовании jetpack compose.

Я пишу с нуля программу аренды велосипедов. Соответственно могу использовать современные frameworks и стараться сделать все по феншую :)

Для UI я выбрал jetpack compose и использую MVI для взаимодействия между UI и ViewModel.

Во ViewModel, отвечающую за взаимодействие с Yandex Map, приходят разные события: если клиент двигает карту, с сервера запрашивается информация о находящихся в этом месте велосипедах, 2-х видах парковок и медленных зонах. Кроме того периодически с сервера запрашивается информация об активной аренде клиентом велосипеда. Все эти данные асинхронно передаются на UI.

// это идет c ViewModel на UI
sealed class MapUiState {
    data class Bikes(val bikes: List) : MapUiState()
    data class Parkings(val stations: List, val parkings: List) : MapUiState()
    data class SlowZones(val slowZones: List, val showMarkers: Boolean) : MapUiState()
    data class ActiveRent(val rent: Rent?, val show: Boolean) : MapUiState()
    …
}

// а это c UI в ViewModel 
sealed class MapIntent {
    data class ChangeMapPosition(val mapRect: MapRect, val zoom: Float) : MapIntent()
    …
}

Вроде все хорошо работает, не лагает, jetpack compose достаточно шустрый.

Но я заметил, что иногда некоторая информация не отображается. Например, я вижу по логу, что все необходимые данные получены, но велосипеды почему-то не отобразились. Потом какое-то время все нормально, потом опять что-то не отобразилось. При этом в логе все нормально, никаких ошибок нет.

Достаточно долго пришлось чесать репу и перечитывать документацию чтобы в конце концов наткнутся на то, что было написано на самом видном месте:

«Recomposition is optimistic, which means Compose expects to finish recomposition before the parameters change again. If a parameter does change before recomposition finishes, Compose might cancel the recomposition and restart it with the new parameter.»

Итак если Compose не успел отрисовать всё до следующего изменения, он «с оптимизмом» отбрасывает старое и берется за новое. Признаться, я немного обалдел от такого оптимизма.

Отрисовка в Yandex Map не быстрая, объектов может быть очень много, если есть проблемы на относительно шустром телефоне, понятно что на старом тормозном ведре их будет гораздо больше.

Вроде как решение очевидно, надо не посылать новые события пока не отрисовались старые. Но это значит, что надо добавить новые intents от UI в ViewModel о том, что отрисовка завершена, в ViewModel хранить полученные, но не отправленные на UI данные, в общем много лишнего геморроя и система становится менее понятной и более нагруженной. От этого варианта я отказался.

Следующее что приходит в голову — объединить получаемые данные и посылать их одним событием на отрисовку. Для начала я объединил отрисовку велосипедов, парковок и медленных зон. Т.е. только когда собраны все необходимые данные посылается одно событие на отрисовку.

sealed class MapUiState {
    data class MapContent(val bikes: List?, val stations: List?, val parkings: List?, val slowZones: List?, val showMarkers: Boolean) : MapUiState()
    …
}

Но дальше это надо было еще объединить с данными аренды велосипеда и получилось что-то монстроидальное. И кроме того если пользователь шустро скролит экран, то тяжелая отрисовка все равно не успевает.

Решение пришло неожиданно: надо просто отправлять новые данные от ViewModel на UI с небольшой задержкой, гарантирующей, что предыдущие данные уже отрисовались. Для этого решил использовать SharedFlow.

В ViewModel добавил следующее:

private val mapUiStates: MutableSharedFlow = MutableSharedFlow(replay = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private lateinit var mapUiStatesJob: Job
private val _mapUiState: MutableStateFlow = MutableStateFlow(MapUiState.Normal)
val mapUiState: StateFlow = _mapUiState.asStateFlow()

private fun initStates() {
    mapUiStatesJob = mapUiStates.onEach {
        delay(CHANGE_STATE_DELAY)
        _mapUiState.value = it
    }.launchIn(viewModelScope)
}

private fun closeStates() {
    mapUiStatesJob.cancel()
}

Дальше кидаем события в SharedFlow, которая с задержкой тригерит StateFlow, которая в свою очередь «обзервится» на UI:

ViewModel:

private fun changeState(uiState: MapUiState …) {
	…
    mapUiStates.tryEmit(uiState)
}

UI:

val mapUiState by mapViewModel.mapUiState.collectAsStateWithLifecycle()

Поигрался с задержкой — на моем телефоне все стабильно отрисовывается при задержке в 50 мс., что для данной программы вполне допустимо. Очередь тоже большая не набирается, так что решение для меня подошло.

Надеюсь еще кому-то это сможет пригодится или наведет на мысли о причинах возможных проблем с отрисовкой в jetpack compose и путях их решения.

© Habrahabr.ru