Разработка
10 ошибок, которые Android-разработчики до сих пор допускают при работе с Jetpack Compose
Jetpack Compose — это не столько новый набор инструментов для создания пользовательского интерфейса, сколько новая парадигма, и с ней приходит необходимость отказаться от некоторых старых привычек разработки Android-приложений.
Как человек, который за последние два года проанализировал сотни кодовых баз Jetpack Compose, могу сказать, что одни и те же ошибки повторяются снова и снова, что и побудило меня написать этот пост. Даже опытные Android-разработчики, привыкшие к традиционным View-системам, сталкиваются с ними при переходе на Compose.
Самое худшее? Прежде всего — многие из этих проблем вы не замечаете, пока ваше приложение не попадет в продакшн с реальными пользователями и на множестве различных устройств.
1. Вызов функций внутри Composable
Это самая большая проблема, снижающая производительность, которую я вижу в приложениях Compose:
@Composable
fun UserProfile(userId: String) {
val userData = fetchUserData(userId) // DON'T DO THIS
Text(text = userData.name)
}
Обратите внимание, что вызов функции запускает каждую перекомпозицию. Всегда используйте LaunchedEffect или запоминайте это с помощью соответствующих ключей.
@Composable
fun UserProfile(userId: String) {
var userData by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) {
userData = fetchUserData(userId)
}
userData?.let { user ->
Text(text = user.name)
}
}
2. Игнорирование CompositionLocals
Разработчики в итоге создают кошмар с передачей свойств через посредников, вместо того чтобы использовать CompositionLocal для данных темы, пользовательских настроек или внедрения зависимостей.
// Instead of passing theme through 5 levels of composables
val LocalAppTheme = compositionLocalOf { AppTheme() }
@Composable
fun MyApp() {
CompositionLocalProvider(LocalAppTheme provides currentTheme) {
MainScreen()
}
}
3. Чрезмерное использование поднятия состояния (State Hoisting)
Не все состояния должны находиться на верхнем уровне. Если что-то является локальным состоянием пользовательского интерфейса, например, развернутое/свернутое состояние, состояние фокуса или анимация, храните это в компоненте, в котором оно используется.
@Composable
fun ExpandableCard() {
// This state doesn't need to be hoisted
var isExpanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier.clickable { isExpanded = !isExpanded }
) {
// Card content
}
}
4. Неправильное понимание ключей remember()
Запоминание без ключей приводит к устаревшим данным и сбивающему с толку поведению:
@Composable
fun ProductList(products: List<Product>) {
// Wrong: remembers first products list forever
val processedProducts = remember { processProducts(products) }
// Right: recomputes when products change
val processedProducts = remember(products) { processProducts(products) }
}
5. Создание объектов в теле композабл
Это приводит к пустой работе, поскольку каждая перекомпозиция создает новые объекты, как в этом примере:
@Composable
fun AnimatedIcon() {
// Creates new AnimationSpec every recomposition
val animationSpec = tween<Float>(durationMillis = 300)
// Better: remember expensive objects
val animationSpec = remember { tween<Float>(durationMillis = 300) }
}
6. Игнорирование порядка модификаторов
Порядок модификаторов очень важен, поскольку он может повлиять на компоновку и скорость работы. Сначала фон, затем отступы, затем кликабельные элементы, затем ограничения размера:
// Wrong order - padding affects clickable area
Modifier
.padding(16.dp)
.clickable { }
.background(Color.Blue)
// Correct order
Modifier
.clickable { }
.background(Color.Blue)
.padding(16.dp)
7. Не использование derivedStateOf
Извлекайте производное состояние с помощью derivedStateOf, когда состояние зависит от другого состояния, чтобы избежать ненужной перекомпозиции:
@Composable
fun FilteredList(items: List<Item>, query: String) {
// Recomputes on every recomposition
val filteredItems = items.filter { it.name.contains(query) }
// Better: only recomputes when dependencies change
val filteredItems by remember {
derivedStateOf { items.filter { it.name.contains(query) } }
}
}
8. Блокировка потока композиции
Композиция выполняется в основном потоке. Тяжелые вычисления при композиции приводят к зависанию пользовательского интерфейса:
@Composable
fun ExpensiveCalculation(data: List<Int>) {
// Blocks composition
val result = data.map { heavyComputation(it) }
// Better: move to background
var result by remember { mutableStateOf<List<Int>>(emptyList()) }
LaunchedEffect(data) {
result = withContext(Dispatchers.Default) {
data.map { heavyComputation(it) }
}
}
}
9. Неправильное управление побочными эффектами
Неправильное использование API для управления побочными эффектами = утечки памяти и сбои.
LaunchedEffect: для функций приостановкиDisposableEffect: для очистки (слушатели, наблюдатели)SideEffect: для API, не использующих Compose, которые требуют последних значений
@Composable
fun LocationTracker() {
DisposableEffect(Unit) {
val locationListener = createLocationListener()
locationManager.requestLocationUpdates(locationListener)
onDispose {
locationManager.removeUpdates(locationListener)
}
}
}
10. Отсутствие тестирования рекомпозиций
Тестирование Compose так же, как и обычных представлений, приводит к тому, что ошибки, связанные с рекомпозициями, остаются незамеченными. Используйте специальные инструменты тестирования:
@Test
fun testRecompositionBehavior() {
var recompositionCount = 0
composeTestRule.setContent {
RecompositionCounter { recompositionCount++ }
MyComposable()
}
// Assert recomposition count expectations
}
Путь к Compose мастерству
Команда разработчиков Android в Google заявила, что приложения, следующие этим принципам, испытывают на 40% меньше проблем с производительностью и получают гораздо более высокие оценки пользовательского опыта.
Ключевой вывод? Jetpack Compose — это не столько новый набор инструментов для создания пользовательского интерфейса, сколько новая парадигма, и с ней приходит необходимость отказаться от некоторых старых привычек разработки Android-приложений.
Используйте это как отправную точку для проверки вашего текущего кода Compose на наличие подобных проблем. Уделите время исправлению каждой ошибки по отдельности и используйте инструменты отладки Compose в Android Studio, чтобы убедиться, что вы решаете проблемы.
Разработчики, которые освоят подобные концепции на раннем этапе, будут создавать плавные и производительные Android-приложения, которые нравятся пользователям и которые конкуренты не смогут превзойти.
