Одна из моих любимых тем в Compose-разработке — отладка рекомпозиций. Layout Inspector, Rebugger, ComposeInvestigator — всё это хорошие инструменты, но у них общий слепой угол: они говорят сколько рекомпозиций, но не отвечают на вопрос нормально ли это для данного компонента.
HomeScreen, который рекомпозируется 10 раз в секунду — это проблема. Анимация, которая делает то же самое — это норма. Число одинаковое, вывод противоположный.Библиотека работает на основе Kotlin compiler plugin, который классифицирует каждый
@Composable по роли и назначает ему бюджет рекомпозиций:1️⃣ Screen — 3/s. Если экранный компонент рекомпозируется чаще, state утекает вверх по дереву
2️⃣ Leaf — 5/s.
Text, Icon, Image — дёшевы сами по себе, но не должны «молотить»3️⃣ Animated — 120/s. Всё, что использует
animate*, Transition, Animatable — пусть работает4️⃣ Container — 10/s, Interactive — 30/s, List Item — 60/s
При скролле бюджеты удваиваются, при анимации и вводе — умножаются на 1.5. Контекст учитывается.
Когда что-то выходит за рамки, в логах появляется не просто число, а конкретика:
BUDGET VIOLATION: ProfileHeader rate=11/s exceeds LEAF budget=5/s
-> params: avatarUrl=CHANGED, displayName=CHANGED
-> forced: 0 | param-driven: 11 | interaction: IDLE
@Composable. Никакого переключения контекста.Попробую на своих проектах (только не отправляй в прод) — идея с контекстными бюджетами кажется мне намного честнее, чем единый порог для всех компонентов. Решение пока не достигло версии 1.0 но это и некритично, так как не влияет на продакшен код.
🔗 Источник: adital.dev
#Android #Compose #AndroidDev #Производительность
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
Media is too big
VIEW IN TELEGRAM
Tracey записывает все жесты, переходы между экранами и кастомные события в кольцевой буфер — и при краше сохраняет его для воспроизведения. Видишь не просто стектрейс, а весь путь пользователя до момента падения.
Не сразу понял куда её применить, но пришла идея интеграции в флоу автоматического прокликивания экрана:
Разрабатываешь фичу локально, кликаешь руками, что-то идёт не так. Вместо того чтобы объяснять разработчику или агенту на словах "я нажал сюда, потом перешёл туда, потом кнопка не сработала" — просто скидываешь ему дамп сессии из Tracey. Он сам восстанавливает картину и сразу работает с контекстом, а не с твоим пересказом.
Структурированный контекст для дебаг-сессии с агентом, чтобы дать четкую информацию.
Библиотека на версии 0.0.2, только вышла, в продакшен пока не потащу. Но для этапа разработки и связки с AI-агентами идея выглядит рабочей.
#Compose #Android #AndroidDev
Please open Telegram to view this post
VIEW IN TELEGRAM
Да, у нас уже есть Jetpack Navigation 3 с официальной поддержкой KMP. Тем не менее авторы Kompass смотрят на проблему иначе, и идея тут любопытная.
Суть в том, что вся навигация строится на чистых редукторах. Любой переход — это
State + Command → State, без побочных эффектов. Вот как это выглядит в коде:// Граф описывает, какие экраны в нём живут и как рендерятся
class MainNavigationGraph : NavigationGraph {
override fun canResolveDestination(id: String) =
id in setOf("home", "profile")
@Composable
override fun Content(entry: BackStackEntry, destination: Destination, navController: NavController) {
when (destination) {
is MainDestination.Home -> HomeScreen(navController)
is MainDestination.Profile -> ProfileScreen(navController)
}
}
}
// Хост принимает список графов и рендерит текущий экран
@Composable
fun AppNavigation() {
val navController = rememberNavController(
startDestination = MainDestination.Home
)
KompassNavigationHost(
navController = navController,
graphs = persistentListOf(MainNavigationGraph())
)
}
// Навигация из экрана
navController.navigate(
entry = BackStackEntry(destinationId = "profile", scopeId = newScope())
)
// Возврат с результатом
navController.pop(result = ProfileResult(userId = "123"))
NavigationState и BackStackEntry иммутабельны, поэтому всю навигацию можно покрыть обычными unit-тестами без инструментации: создаёшь NavigationHandler, кидаешь команду, проверяешь стейт.Особенности:
👉 Таргеты — Android, iOS, Desktop (JVM).
👉 Scopes вместо ViewModel.
rememberScoped<T> живёт ровно пока BackStackEntry в стеке, автоматически чистится при pop.👉 Multi-graph. Несколько независимых графов с собственными лейаутами. Из коробки есть поддержка master-detail для планшетов.
👉 Дип-линки. Через
DeepLinkHandler — типизированный парсинг URI в NavigationCommand.🛠 Библиотека ещё в активной разработке
#KMP #ComposeMultiplatform #Navigation #Kotlin #AndroidDev
Please open Telegram to view this post
VIEW IN TELEGRAM
Если composable нужен только для превью из другого модуля,
internal не поможет. Зато поможет кастомная аннотация поверх @RequiresOptIn с уровнем ERROR.kotlin@PreviewOnly
@Composable
fun renderScreenPreviewer(list: List) {
renderList(list)
}
@RequiresOptIn(
message = "This composable is intended for preview usage only",
level = RequiresOptIn.Level.ERROR
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.FUNCTION)
annotation class PreviewOnly
Теперь любой, кто попробует вызвать его в продакшн коде без
@OptIn(PreviewOnly::class), получит ошибку компиляции, а не warning.Небольшой минус:
@OptIn(PreviewOnly::class) придётся писать в каждой превью-функции вручную, автоматического opt-in для @Preview нет. Но это копейки по сравнению с тем, что превью-composable физически не попадёт в продакшн.Источник le0nidas.gr
#Compose
Please open Telegram to view this post
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
Самое интересное из нового: поддержка Shared Elements между экранами. Теперь достаточно обернуть
NavDisplay в SharedTransitionLayout и передать scope:SharedTransitionLayout {
NavDisplay(
backStack = backStack,
sharedTransitionScope = this
)
}Второе большое добавление —
SceneDecoratorStrategy. Отдельный интерфейс для декорирования сцен общими UI-элементами или для шаринга состояния между ними. Логика декорирования теперь отделена от логики выбора сцены в SceneStrategy.Type safe metadata DSL через
MetadataKey. Больше не нужны строковые ключи — типы значений описаны статически. Готовые NavMetadataKey позволяют задавать анимации переходов прямо в entryProvider:entry<ScreenC>(
metadata = metadata {
put(NavDisplay.TransitionKey) {
slideInVertically { it } togetherWith fadeOut()
}
}
) { ... }
#compose #navigation3 #jetpack
Please open Telegram to view this post
VIEW IN TELEGRAM
Что нового:
👉 Визуальная отладка переходов Shared Elements
👉 Переработанная поддержка трекпадов — теперь работают как мышь и распознают жесты
👉 Preview Wrappers для более быстрой работы с превью
👉 Host Defaults для KMP-проектов
👉 Testing APIs v2 для корутин
👉 Экспериментальные API: Styles, MediaQuery, Grid, FlexBox
Подробности по каждому пункту — отдельными постами в @compose_broadcast
#Compose #Android #AndroidJetpack
Please open Telegram to view this post
VIEW IN TELEGRAM
В Compose 1.11.0 завезли новый интерфейс PreviewWrapper и аннотацию @PreviewWrapperProvider. Идея простая: вместо того чтобы оборачивать каждый Preview в свою тему, CompositionLocalProvider, моки навигации и прочее, ты один раз описываешь обёртку и просто навешиваешь её аннотацией.
Выглядит это так:
class ThemeWrapper : PreviewWrapper {
@Composable
override fun Wrap(content: @Composable () -> Unit) {
JetsnackTheme {
content()
}
}
}@PreviewWrapperProvider(ThemeWrapper::class)
@Preview
@Composable
private fun ButtonPreview() {
Button(onClick = {}) {
Text("Demo")
}
}
Сценарии где может пригодиться:
👉 встроить тему в превью
👉 подложить FakeNavController или LocalActivity
👉 обернуть в Hilt/Koin провайдер с тестовыми зависимостями
👉 прокинуть моки через CompositionLocal
Раньше для этого либо городили собственную функцию и вызывали её руками в каждом превью, либо писали кастомные
@Preview аннотации с ограниченным набором параметров.‼️ ВАЖНО: фича работает только в Android Studio Panda 4 и выше. В более старых версиях аннотация просто проигнорируется.
#compose #jetpackcompose #android
Please open Telegram to view this post
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
#Compose
Please open Telegram to view this post
VIEW IN TELEGRAM
// Пример использования
LookaheadAnimationVisualDebugging(
overlayColor = Color(0x4AE91E63),
isEnabled = true,
multipleMatchesColor = Color.Green,
isShowKeylabelEnabled = false,
unmatchedElementColor = Color.Red,
) {
SharedTransitionLayout {
CompositionLocalProvider(
LocalSharedTransitionScope provides this,
) {
// your content
}
}
}
#Compose
До недавнего времени стилизация компонентов в Compose делалась через параметры Composable функции (
color = ..., padding = ...) или модификаторы. Это работало, но могло приводить к:👉 избыточным рекомпозициям при смене состояний;
👉 дублированию кода для разных вариаций кнопок/карточек;
👉 сложностям с анимацией переходов между стилями.
Google представил Styles API (экспериментальный, о причинах такого статуса ниже) — новый подход, вдохновлённый CSS. Нужен Jetpack Compose 1.11.0 или свежее
Ключевые возможности
Стили вычисляются на этапах layout и drawing, минуя composition. Это значит, что изменение стиля (например, при наведении) не вызывает рекомпозицию родительских компонентов.
Без кучи
if/else и animate*AsState. Просто описываете стиль для каждого состояния (нажат, сфокусирован, включён, ошибка и т.д.).Переходы между состояниями анимируются автоматически с физическими параметрами (демпфирование, жёсткость). Не нужно писать
Animatable или Transition.// Пример использования стиля
// Определяем стиль для кнопки
val buttonStyle = Style {
default {
backgroundColor = Color.Blue
shape = RoundedCornerShape(8.dp)
}
pressed {
backgroundColor = Color.DarkBlue
scale = 0.98f
}
disabled {
backgroundColor = Color.Gray
alpha = 0.5f
}
}
// Используем
Button(
onClick = { /* ... */ },
style = buttonStyle, // ← вместо кучи параметров
enabled = isEnabled
) {
Text("Нажми меня")
}
style — новый параметр, который появляется у всех компонентов Material 3 и базовых (Box, Row, Column).⚠️ Styles API не заменяет модификаторы
Модификаторы остаются для позиционирования, кликов, размеров. Styles API заменяет только внутреннюю стилизацию (цвета, формы, тени, отступы внутри компонента). Это как разделение «темы» и «вёрстки».
API остаётся экспериментальным пока на него полностью не перейдет Material 3 Compose, чтобы учесть все необходиcмоти API и не делать breaking changes. По работе оно уже стабильно, но вот изменения в коде, возможно, будут.
🔗 Официальная документация
В целом с таким подходом уже привыкли работать в Android XML Layout, то почему бы сразу было его не использовать? Тем более в Compose плюсы очевидны!
#Compose #Material
Please open Telegram to view this post
VIEW IN TELEGRAM
До сих пор для сеток у нас были только ленивые
LazyVerticalGrid / LazyHorizontalGrid. Но если все элементы уже в памяти (небольшое количество) и нужен точный контроль над расположением ячеек — пригождается новый экспериментальный Grid.// Пример
Grid(config = {
column(100.dp)
column(1.fr) // забирает остаток строки
row(50.dp)
row(80.dp)
gap(8.dp)
}) {
Box(Modifier.gridItem().fillMaxSize()) { ... }
Box(Modifier.gridItem().fillMaxSize()) { ... }
Box(Modifier.gridItem().fillMaxSize()) { ... }
Box(Modifier.gridItem().fillMaxSize()) { ... }
}
Работает на Android и KMP (iOS, Desktop, Web) – общий API
🔗 Документация по Grid
#Compose #KMP #Android #iOS #Desktop #Web #CMP
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
В составе
androidx.compose.foundation:layout 1.11.0 появился компонент FlexBox. Компоновка на основе правил CSS Flexbox. Поддерживает перенос элементов (wrap), веса (weight), выравнивание по главной и поперечной осям. Сразу вышел с поддержкой Compose Multiplatform.FlexBox(
modifier =
Modifier.fillMaxWidth(),
config = {
flexDirection = Row
flexWrap = Wrap
justifyContent = SpaceBetween
}
) {
repeat(items.size) { index ->
Text(
text = items[index],
modifier =
Modifier.weight(1f)
)
}
}
🔗 Анонс в блоге Android Developers
🔗 API Reference
#Compose #CMP
Please open Telegram to view this post
VIEW IN TELEGRAM
В апрельском релизе Jetpack Compose под номером 1.11 в compose-runtime завезли четыре новых API:
HostDefaultProvider, LocalHostDefaultProvider, HostDefaultKey и ViewTreeHostDefaultKey. Плюс билдер compositionLocalWithHostDefaultOf для авторов библиотек.Он позволяют объявить
CompositionLocal, у которого дефолтное значение резолвится из окружения хоста — например, через Android View tag'и (как это делает ViewTreeLifecycleOwner или ViewTreeOnBackPressedDispatcherOwner). Раньше для такого паттерна была обязательна зависимость на compose-ui. Теперь только compose-runtime.Раньше чтобы из библиотеки достать значение из View tree, приходилось тянуть compose-ui ради LocalView и платформенные функции поиска по дереву View. Код жил в Android-сорсах, в commonMain такое не положишь:
// androidMain — compose-ui + Android-only API
val LocalMyOwner = compositionLocalOf<MyOwner?> { null }
@Composable
fun ProvideMyOwner(content: @Composable () -> Unit) {
val view = LocalView.current
val owner = remember(view) {
view.findViewTreeMyOwner()
}
CompositionLocalProvider(LocalMyOwner provides owner) {
content()
}
}
С новым API описание CompositionLocal живёт в commonMain и тянет только compose-runtime. Связку с View tree описывает ViewTreeHostDefaultKey, и хост сам отдаёт значение через своего провайдера:
// commonMain — только compose-runtime
val MyOwnerKey = ViewTreeHostDefaultKey<MyOwner>(
viewTagId = R.id.view_tree_my_owner
)
val LocalMyOwner = compositionLocalWithHostDefaultOf(MyOwnerKey)
// Использование — без изменений
@Composable
fun MyComponent() {
val owner = LocalMyOwner.current
// ...
}
Теперь авторы Compose библиотек могут держать определение CompositionLocal в общем модуле без зависимости на compose-ui. Профит - меньше транзитивных зависимостей в KMP-библиотеках и более чистое разделение слоёв runtime и UI.
#Compose #CMP
Please open Telegram to view this post
VIEW IN TELEGRAM
Главные фичи:
Breaking изменения, которые заденут почти всех:
👉 Минималка Kotlin поднята до 2.3.10
👉 Минимальная iOS теперь 14 (поддержка 13 ушла)
👉 iosX64 и macosX64 удалены полностью
👉 Shader на не-Android стал wrapper-классом
Я ждал нативный input на iOS с того момента, как Compose туда заехал. Текущая реализация на Skia всегда чувствовалась чужой на ощупь, особенно при сложном выделении и системных меню. Надо пробовать!
#compose #cmp #kotlin
Please open Telegram to view this post
VIEW IN TELEGRAM