Разработка
Устранение задержек в Composable: 3 практических метода для создания плавного UI
Надеюсь, когда придёт время, вы вспомните представленные здесь решения и сочтёте их полезными.
Мы часто совершаем ошибку, выполняя сложные операции внутри Composable-объектов.
Однако Composable-объект — это всего лишь функция, которая конструирует пользовательский интерфейс и работает в основном потоке.
Это означает, что во время выполнения Composable-объекта весь процесс конструирования и отрисовки дерева пользовательского интерфейса происходит в основном потоке.
Но что произойдёт, если в этот момент параллельно будут выполняться сложные операции?
Обновление пользовательского интерфейса не успеет обработаться вовремя, что приведёт к кратковременному зависанию или подтормаживанию экрана.
Это называется «задержками».
FrameTime
FPS (кадры в секунду) варьируется в зависимости от устройства, но, как правило, Android работает со скоростью 60 кадров в секунду.
Это означает, что 60 кадров должны быть отрисованы в течение 1 секунды (1000 мс), поэтому время, доступное для обработки одного кадра, следующее:
1000 / 60 = 16.63 мс
Другими словами, для плавного отображения каждое обновление пользовательского интерфейса должно завершаться в течение 16.63 мс.
Превышение этого времени приводит к накоплению кадров, что приводит к заметным подтормаживаниям.
Пример задержки
Давайте рассмотрим следующий код:
@Composable
fun Test() {
LazyColumn(
modifier = Modifier
.statusBarsPadding()
.height(300.dp)
.fillMaxWidth()
) {
item {
Text(
text = "Janky Ui",
fontSize = 32.sp
)
}
items(100) {
Text(computeHeavy(it))
}
}
}
fun computeHeavy(value: Int): String {
Thread.sleep(100)
return "$value computed!"
}
При каждой отрисовке элемента LazyColumn вызывается computeHeavy(), что приводит к полной остановке обновления пользовательского интерфейса, пока эта функция выполняется в основном потоке. Сто миллисекунд значительно превышают требуемое время на кадр в 16.63 мс, что приводит к откладыванию нескольких кадров и серьёзным подтормаживаниям экрана (задержкам).
Другими словами, пользовательский интерфейс всё ещё пытается отрисовываться с частотой 60 кадров в секунду, но основной поток слишком занят вычислениями, чтобы выделить время на отрисовку.
Итак, как следует обрабатывать тяжёлые задачи, чтобы избежать задержек?
Вот 3 способа решения этой проблемы в среде Compose.
Обработка во ViewModel и отображение в UI
Лично я бы выбрал этот метод в первую очередь.
Тяжёлые задачи обрабатываются асинхронно во ViewModel, а пользовательский интерфейс просто отображает результаты, что приводит к чистому коду и чёткому распределению обязанностей.
class TestViewModel : ViewModel() {
private val _computed = MutableStateFlow<List<String>>(emptyList())
val computed = _computed.asStateFlow()
init {
viewModelScope.launch {
println("launch: ${Thread.currentThread().name}")
withContext(Dispatchers.IO) {
val results = (0 until 100).map {
async {
println("Async: ${Thread.currentThread().name}")
computeHeavy(it)
}
}.awaitAll()
_computed.update { results }
}
}
}
}
@Composable
fun Test(viewModel: TestViewModel) {
val computed by viewModel.computed.collectAsState()
LazyColumn(
modifier = Modifier
.statusBarsPadding()
.height(300.dp)
.fillMaxWidth()
) {
item {
Text(
text = "viewModel",
fontSize = 32.sp
)
}
items(computed) {
Text(it)
}
}
}
Важно отметить, что viewModelScope по умолчанию использует Dispatchers.Main.immediate. Это означает, что если вы явно не укажете другой диспетчер, все корутины, запущенные в нём, будут выполняться в основном потоке.
Поэтому необходимо явно переключить тяжёлые задачи на Dispatchers.IO или Dispatchers.Default, чтобы избежать их запуска в MainThread и предотвратить задержки обновления.

Когда переключение контекста не выполняется с помощью WithContext

Когда переключение контекста выполняется с помощью WithContext
Асинхронная обработка с помощью ProduceState
ProduceState, который асинхронно извлекает состояние, может быть эффективным решением.
@Composable
fun Test() {
val computed by produceState(emptyList()) {
value = withContext(Dispatchers.IO) {
(0 until 100).map {
async { computeHeavy(it) }
}.awaitAll()
}
}
LazyColumn(
modifier = Modifier
.statusBarsPadding()
.height(300.dp)
.fillMaxWidth()
) {
item {
Text(
text = "produceState",
fontSize = 32.sp
)
}
items(computed) {
Text(it)
}
}
}
В produceState можно использовать suspend для асинхронной инициализации, что делает этот подход эффективным.
В этом случае также обязательно переключите контекст с помощью Dispatchers.IO или Dispatchers.Default.
Асинхронная обработка через LaunchedEffect
Использование LaunchedEffect для обработки внутри EffectHandler также является допустимым подходом.
@Composable
fun Test() {
val computed = remember { mutableStateListOf<String>() }
LaunchedEffect(Unit) {
val results = withContext(Dispatchers.IO) {
(0 until 100).map {
async { computeHeavy(it) }
}.awaitAll()
}
computed.addAll(results)
}
LazyColumn(
modifier = Modifier
.statusBarsPadding()
.height(300.dp)
.fillMaxWidth()
) {
item {
Text(
text = "LaunchedEffect",
fontSize = 32.sp
)
}
items(computed) {
Text(it)
}
}
}
LaunchedEffect также должен переключать контексты с помощью Dispatchers.IO или Dispatchers.Default.
ProduceState или LaunchedEffect: какой из них использовать?
produceState внутри использует LaunchedEffect:

Реализация produceState
Но, как следует из названия, она ориентирована на создание состояния.
Подумайте об этом. Разве не странно было бы представить себе какой-то эффект, возникающий при каждой инициализации состояния?
Поэтому, если вам просто нужно асинхронно инициализировать состояние, использование produceState вполне уместно.
С другой стороны, если вам нужны дополнительные эффекты наряду с инициализацией, использование LaunchedEffect предпочтительнее.
В заключение
Редко возникает необходимость выполнять сложные задачи непосредственно в пользовательском интерфейсе, и большая часть обработки будет выполняться в ViewModel.
Однако, если у вас много работы или вам не хватает опыта с Compose, вам может потребоваться выполнять значительный объём кода внутри Composable-объектов для отображения состояния в пользовательском интерфейсе.
Надеюсь, когда придёт время, вы вспомните представленные здесь решения и сочтёте их полезными.
-
Аналитика магазинов2 недели назад
Мобильный рынок Ближнего Востока: исследование Bidease и Sensor Tower выявляет драйверы роста
-
Интегрированные среды разработки3 недели назад
Chad: The Brainrot IDE — дикая среда разработки с играми и развлечениями
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2025.45
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.46

