Что выбрать для навигации в Compose🤨
Это довольно распространенный вопрос и на сегодняшний день выбор либ просто огромен на любой вкус и цвет. Так что же выбрать? Конечно жеDecompose решать вам на основе требований к навигации в вашем приложении.
🤖 Jetpack Compose Navigation — официальная библиотека
👍 Поддержка от Google, интеграция с ViewModel
👎 Только для Android и еще миллион минусов
🤖 Jetpack Compose Destinations — обертка над официальной либой
👍 Решает некоторые проблемы первой либы
👎 Добавляет новых проблем из-за кодогенерации и зависимости на accompanist либы
🤖 Modo — либа от создателя Cicerone, Константина Цховребова
👍 Строится на принципах UDF, очень простая
👎 Только для Android, еще не в релизе, маленькое коммьюнити
👩💻 Appyx — решение для навигации от Bumble
👍 Декларативный подход, классные анимации из коробки, поддержка KMP
👎 Только для Compose, довольно сложная, небольшое коммьюнити
👩💻 Voyager — популярная и простая либа для навигации
👍 Много интеграций с привычными инструментами, легкая в использовании, поддержка KMP
👎 Только для Compose, есть проблемы со стабильностью
🌳 Decompose — либа от Аркадия Иванова, автора MVIKotlin
👍 Единственное решение не завязанное на UI фреймворк, декларативный подход, огромная гибкость, высокая стабильность
👎 Высокий порог входа, приходится писать много кода
Есть еще решение Odyssey от Алексея Гладкова, но автор объявил о прекращении поддержки данной либы.
Таким образом настоятельно не рекомендую использовать официальное решение для навигации в любых более менее сложных приложениях. Можете посмотреть мой доклад, где я сравниваю эту либу и Decompose. Другие либы можно смело у себя использовать, но если вы проникнетесь подходом к навигации в Decompose, то можете посмотреть другой доклад, где уже разбираю как интегрироваться не только с Compose, но и SwiftUI.
Как вы поняли, я очень топлю за Decompose, хотя его сложность может многих отпугнуть, но стоит только проникнуться компонентым подходом и уже по-другому приложения писать не захочется!
#Compose #Navigation
Это довольно распространенный вопрос и на сегодняшний день выбор либ просто огромен на любой вкус и цвет. Так что же выбрать? Конечно же
Есть еще решение Odyssey от Алексея Гладкова, но автор объявил о прекращении поддержки данной либы.
Таким образом настоятельно не рекомендую использовать официальное решение для навигации в любых более менее сложных приложениях. Можете посмотреть мой доклад, где я сравниваю эту либу и Decompose. Другие либы можно смело у себя использовать, но если вы проникнетесь подходом к навигации в Decompose, то можете посмотреть другой доклад, где уже разбираю как интегрироваться не только с Compose, но и SwiftUI.
Как вы поняли, я очень топлю за Decompose, хотя его сложность может многих отпугнуть, но стоит только проникнуться компонентым подходом и уже по-другому приложения писать не захочется!
#Compose #Navigation
Please open Telegram to view this post
VIEW IN TELEGRAM
Декларативный Bottom Sheet
На мой взгляд, самый неудачный компонент в Compose из Material 2 — это Bottom Sheet. Он долгое время крашился при изменении конфигурации, его кучу раз переписывали, но все-равно на сегодняшний день он содержит много проблем:
😀 Приходится пилить костыли для работы с WindowInsets
😀 Он не прилипает к низу экрана
😀 Производительность оставляет желать лучшего
😀 Скрытие Scrim нельзя кастомизировать
А самое худшее — это его императивный API, в котором мы вынуждены управлять его показом через suspend функции show/hide, а также приходится оборачивать контент экрана в
😀 Решить проблему можно с помощью кастомной декларативной обертки
Как это работает
Показываем Bottom Sheet, если ассоциированный с ним стейт ≠ null, иначе скрываем
Особенности реализации
Нужно уметь показывать предыдущий контент, пока bottom sheet скрывается, несмотря на то, что данных уже нет
Нужно правильно вызывать лямбду
😀 Завязываться на
😀 Отслеживание изменения
Проблема😀
На текущий момент если скрыть Bottom Sheet через изменение стейта, то его скрытие можно перехватить жестом и тогда останемся в неконсистентном состоянии. Решить проблему можно либо скрытием Bottom Sheet без анимации, либо не занулять стейт, пока он не будет полностью скрыт.
😀 Гораздо лучше дела обстоят в Material 3, там из коробки Bottom Sheet уже декларативный и в нем было решено большинство проблем, только вот далеко не все используют M3 в своих проектах и, чтобы использовать его реализацию, придется копировать к себе кучу исходного кода, что тоже не круто. Таким образом если вы еще не перешли на компоузовский Bottom Sheet, то лучше пока и не торопиться😉
А как вы боретесь с проблемами с Bottom Sheet в своем проекте?
#Compose
На мой взгляд, самый неудачный компонент в Compose из Material 2 — это Bottom Sheet. Он долгое время крашился при изменении конфигурации, его кучу раз переписывали, но все-равно на сегодняшний день он содержит много проблем:
А самое худшее — это его императивный API, в котором мы вынуждены управлять его показом через suspend функции show/hide, а также приходится оборачивать контент экрана в
ModalBottomSheetLayout
. Это очень не удобно, когда нужно показать не статический контент, а полноценный экран с динамическим отображением данных и своей логикой. Как это работает
Показываем Bottom Sheet, если ассоциированный с ним стейт ≠ null, иначе скрываем
Особенности реализации
Нужно уметь показывать предыдущий контент, пока bottom sheet скрывается, несмотря на то, что данных уже нет
Нужно правильно вызывать лямбду
onDismiss
и здесь можно допустить ошибку:confirmValueChange
не вариант, так как теперь этот callback вызывается множество разsheetState.targetValue
также может привести к проблемам, так как targetValue
будет Hidden
даже если вы не до конца скрыли Bottom SheetПроблема
На текущий момент если скрыть Bottom Sheet через изменение стейта, то его скрытие можно перехватить жестом и тогда останемся в неконсистентном состоянии. Решить проблему можно либо скрытием Bottom Sheet без анимации, либо не занулять стейт, пока он не будет полностью скрыт.
А как вы боретесь с проблемами с Bottom Sheet в своем проекте?
#Compose
Please open Telegram to view this post
VIEW IN TELEGRAM
Compose Snapshots
❓ После доклада меня много спрашивали, а зачем мне вообще знать про снапшоты? Где эти знания применить на практике?
💡 И на мой взгляд, в первую очередь их нужно знать, чтобы разобраться, а каким образом изменение State внутри Composable функции приводит к рекомпозиции?
✳️ Давайте сначала вспомним, что вообще такое Snapshot
Это механизм, используемый внутри Compose State и не только, для работы с множественными изолированными копиями состояния. Снапшоты также позволяют сделать безопасное изменение стейта конкурентно без блокировок. Можете думать про снапшоты как про транзакции в базах данных или как про ветки в git.
✳️ Причем тут снапшоты и рекомпозиция?
Все Composable дерево заворачивается в Snapshot, поэтому на самом деле изменение стейта не происходит глобально, а происходит внутри снапшота, поэтому снапшот может отследить каждое чтение и запись любого стейта внутри, так Compose понимает какие части экрана нужно перерисовать в дальнейшем.
✳️ Где снапшоты могут пригодиться на практике?
🟠 Если вдруг вы захотите написать свою либу поверх compose runtime
🟡 Для безопасного изменения стейта с нескольких потоков
🔵 Для создания своего изменяемого Stable типа, отслеживаемого Compose
🔵 Для всего, на что способно ваше воображение! Например, почему бы не работать с БД через Compose State? Что? Да! Это тоже возможно благодаря снапшотам.
Но самое крутое и полезное применение снапшотов я покажу в следующем посте, так что stay tuned💻
#Compose #Snapshots
Это механизм, используемый внутри Compose State и не только, для работы с множественными изолированными копиями состояния. Снапшоты также позволяют сделать безопасное изменение стейта конкурентно без блокировок. Можете думать про снапшоты как про транзакции в базах данных или как про ветки в git.
Все Composable дерево заворачивается в Snapshot, поэтому на самом деле изменение стейта не происходит глобально, а происходит внутри снапшота, поэтому снапшот может отследить каждое чтение и запись любого стейта внутри, так Compose понимает какие части экрана нужно перерисовать в дальнейшем.
private inline fun <T> composing(
composition: ControlledComposition,
modifiedValues: IdentityArraySet<Any>?,
block: () -> T
): T {
val snapshot = Snapshot.takeMutableSnapshot(
readObserverOf(composition), writeObserverOf(composition, modifiedValues)
)
try {
return snapshot.enter(block)
} finally {
applyAndCheck(snapshot)
}
}
Но самое крутое и полезное применение снапшотов я покажу в следующем посте, так что stay tuned
#Compose #Snapshots
Please open Telegram to view this post
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
Compose withAnimation
В SwiftUI есть очень классная фича
Не справедливо, что такого механизма нет из коробки в Compose, и инженер из Google решил исправить это недоразумение. Он сделал свой аналог
Как это работает?
1. Создается пустой словарь состояний для анимации
2. Выполняется лямбда блок внутри Snapshot, в этой лямбде могут происходить изменения стейта
3. У Snapshot вызывается
4. Данные мапятся в другой тип, откуда достаются измененные значения
5. Уничтожается Snapshot, чтобы не допустить утечек памяти, при этом изменения не применяются глобально! «Все что произошло в снапшоте, остается в снапшоте»©
6. Анимируются значения
Если у вас есть еще идеи как можно применить снапшоты, делитесь своими мыслями в комментариях👇
#Compose #Snapshots #Animations
В SwiftUI есть очень классная фича
withAnimation
, позволяющая сделать анимацию вьюшки просто путем изменения состояния, а сама анимация произойдет как по волшебству.
@State private var showDetail = false
var body: some View {
VStack {
Button("Show details") {
withAnimation {
showDetail.toggle()
}
}
if showDetail {
Text("Details")
}
}
}
Не справедливо, что такого механизма нет из коробки в Compose, и инженер из Google решил исправить это недоразумение. Он сделал свой аналог
withAnimation
и реализовал это с помощью Snapshot API, про который мы говорили ранее.Как это работает?
1. Создается пустой словарь состояний для анимации
2. Выполняется лямбда блок внутри Snapshot, в этой лямбде могут происходить изменения стейта
3. У Snapshot вызывается
writeObserver
при каждой записи в State и заполняется информация для анимации4. Данные мапятся в другой тип, откуда достаются измененные значения
5. Уничтожается Snapshot, чтобы не допустить утечек памяти, при этом изменения не применяются глобально! «Все что произошло в снапшоте, остается в снапшоте»©
6. Анимируются значения
internal suspend fun withAnimation(
adapterRegistry: StateObjectAdapterRegistry,
animationSpec: AnimationSpec<Any?>,
block: () -> Unit
) {
val statesToAnimate = mutableMapOf<Any, StateObjectAdapter>() // 1
val snapshot = Snapshot.takeMutableSnapshot(
writeObserver = { changedState ->
statesToAnimate[changedState] = checkNotNull(adapterRegistry.getAdapterFor(changedState)) // 3
}
)
val targetValues = snapshot.enter {
block() // 2
buildTargetValues(statesToAnimate) // 4
}
snapshot.dispose() // 5
animateValues(targetValues, animationSpec) // 6
}
Если у вас есть еще идеи как можно применить снапшоты, делитесь своими мыслями в комментариях👇
#Compose #Snapshots #Animations
Forwarded from Compose Broadcast (Alex Panov)
This media is not supported in your browser
VIEW IN TELEGRAM
Автор крутого доклада про компиляторные плагины для Compose с предыдущего Mobius опубликовал исходники плагинов на GitHub.
Там очень много всего интересного и полезного:
👉 Анализ стабильности параметров Composable функции
👉 Подсветка рекомпозиций в UI
👉 Автоматическая генерация и удаление testTag
👉 Логирование причин рекомпозиции и другое
Эти плагины наконец-то решают извечную проблему анализа лишних рекомпозиций и оптимизаций вашего кода в Compose, теперь делать высокопроизводительные приложения стало гораздо проще!
#compose #plugins
Там очень много всего интересного и полезного:
👉 Анализ стабильности параметров Composable функции
👉 Подсветка рекомпозиций в UI
👉 Автоматическая генерация и удаление testTag
👉 Логирование причин рекомпозиции и другое
Эти плагины наконец-то решают извечную проблему анализа лишних рекомпозиций и оптимизаций вашего кода в Compose, теперь делать высокопроизводительные приложения стало гораздо проще!
#compose #plugins
Кастомные маски для TextField в Compose
Раньше в Android View реализовать маску для номера телефона, не говоря уже про что-то кастомное было далеко не самой простой задачей и люди, чтобы облегчить себе жизнь, использовали сторонние библиотеки, такие как Decoro.
Но теперь с приходом Compose надобность в сторонних решениях практически отпала, ведь реализовать кастомную маску для TextField можно буквально в 40 строк кода😱 , и это возможно благодаря продуманному и простому API интерфейса
Фишка в том, что
🔸 Определить как исходный текст будет трансформироваться в текст с маской
🔸 Предоставить двухсторонний маппинг для правильного смещения курсора в поле ввода
📘 Подробнее можно почтитать в статье, где разобран случай маски для ввода даты и с полностью кастомизируемой маской
#Compose
Раньше в Android View реализовать маску для номера телефона, не говоря уже про что-то кастомное было далеко не самой простой задачей и люди, чтобы облегчить себе жизнь, использовали сторонние библиотеки, такие как Decoro.
Но теперь с приходом Compose надобность в сторонних решениях практически отпала, ведь реализовать кастомную маску для TextField можно буквально в 40 строк кода
VisualTransformation
.Фишка в том, что
VisualTransformation
, как бы это неожиданно не звучало, влияет всего лишь на визуальное отображение, а не реальное значение поля, и, чтобы реализовать любую маску, достаточно сделать две вещи:
var out = ""
text.text.forEachIndexed { index, char ->
when (index) {
2 -> out += "/$char"
4 -> out += "/$char"
else -> out += char
}
}
val numberOffsetTranslator = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
if (offset <= 2) return offset
if (offset <= 4) return offset + 1
return offset + 2
}
override fun transformedToOriginal(offset: Int): Int {
if (offset <= 2) return offset
if (offset <= 5) return offset - 1
return offset - 2
}
}
#Compose
Please open Telegram to view this post
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
Коллега из Контура, Евгений Мельцайкин, написал статью о том, как сделать такую кнопку с помощью кастомного Layout в Compose и как оптимизировать ее, чтобы достичь минимального количества рекомпозиций. Приятного чтения 📕
Исходный код можно посмотреть здесь🐱
А вы сможете на взгляд определить какая кнопка сделана оптимально по количеству рекомпозиций?
#Compose
Исходный код можно посмотреть здесь
А вы сможете на взгляд определить какая кнопка сделана оптимально по количеству рекомпозиций?
#Compose
Please open Telegram to view this post
VIEW IN TELEGRAM
Скриншот-тестирование в Compose
Google не так давно выкатили свой тулинг для скриншот-тестирования в Compose в экспериментальном режиме и работает он на основе Compose Preview👀
У меня довольно скпептическое отношение к превью, за все время работы с Compose у меня постоянно были какие-то проблемы с этим механизмом, а с приходом Compose Multiplatform заставить превьюшки работать тот ещё челлендж, более менее дела с превью обстоят только в новой IDE Fleet, но там ещё ворох других проблем.
Так вот, вернёмся к тестированию, я попробовал этот способ и что могу сказать по текущему состоянию тулинга:
👍 Официальное решение для скриншот-тестирования
👍 Генерация отчёта с диффом изображений
👍 Уже относительно работает и можно использовать на свой страх и риск
👍 Тесты прогоняются без эмулятора и соответственно прогоняются быстро
👎 Названия сгенерированных скриншотов нельзя поменять
👎 Нельзя выборочно обновить эталонный скриншот
👎 Нужно использовать специальные gradle таски для валидации скриншотов, потребуются доработки на CI
👎 Нельзя настроить минимальный порог отличий между скриншотами
👎 Только для Android
Так что подводя итоги, круто, что появляется решение из коробки, но в текущем состоянии завязываться на него довольно опасно.
#Compose #SnapshotTesting
Google не так давно выкатили свой тулинг для скриншот-тестирования в Compose в экспериментальном режиме и работает он на основе Compose Preview
У меня довольно скпептическое отношение к превью, за все время работы с Compose у меня постоянно были какие-то проблемы с этим механизмом, а с приходом Compose Multiplatform заставить превьюшки работать тот ещё челлендж, более менее дела с превью обстоят только в новой IDE Fleet, но там ещё ворох других проблем.
Так вот, вернёмся к тестированию, я попробовал этот способ и что могу сказать по текущему состоянию тулинга:
Так что подводя итоги, круто, что появляется решение из коробки, но в текущем состоянии завязываться на него довольно опасно.
#Compose #SnapshotTesting
Please open Telegram to view this post
VIEW IN TELEGRAM
Нашли серьезную уязвимость в Jetpack Navigation Compose, которая позволяет открыть любой экран в приложении, даже если там нет явных диплинков ⚠️
Эксплуатируется она максимально просто, достаточно знать имя пакета и название маршрута в графе навигации:
Как защититься
1. Разумеется лучший вариант не использовать данную навигацию, можете посмотреть мой пост со сравнением библиотек навигации для Compose и выбрать подходящую
2. Если в приложении не используются диплинки, можно частично решить проблему перетерев data в определенном intent:
#Security #Compose
@kotlin_adept
Эксплуатируется она максимально просто, достаточно знать имя пакета и название маршрута в графе навигации:
Intent().apply {
setClassName("your.package", "your.package.MainActivity")
data = Uri.parse("android-app://androidx.navigation/YOUR_DESTINATION")
startActivity(this)
}
Как защититься
1. Разумеется лучший вариант не использовать данную навигацию, можете посмотреть мой пост со сравнением библиотек навигации для Compose и выбрать подходящую
2. Если в приложении не используются диплинки, можно частично решить проблему перетерев data в определенном intent:
val intentData = intent.dataString
if (intentData != null && intentData.startsWith("android-app://androidx.navigation")) {
intent.setData(null)
}
#Security #Compose
@kotlin_adept
Please open Telegram to view this post
VIEW IN TELEGRAM
Перевернутые модификаторы
Неудивительно, что Android и iOS разработчики часто не могут найти общий язык, ведь у них (у нас) все перевернуто с ног на голову 🇦🇺
Это касается и модификаторов в декларативных UI фреймворках. На картинке видно, что цепочка из одинаковых модификаторов для Compose и SwiftUI дают один и тот же результат, при этом располагаясь в обратном порядке.
➡️ В Compose первый модификатор size задает минимальные и максимальные констрейнты и мы не можем выйти за эти ограничения, не переопределяя их.
➡️ В SwiftUI таких ограничений нет и там всегда padding применяется во вне, что может быть даже удобнее, так как не приходится об этом задумываться.
🗓 Но к чему я это все? На ближайшей конференции Мобиус буду рассказывать доклад, где сравню ключевые отличия обоих фреймворков, и если тема интересна, то буду рад видеть всех на докладе 😉
#Compose #SwiftUI
@kotlin_adept
Неудивительно, что Android и iOS разработчики часто не могут найти общий язык, ведь у них (у нас) все перевернуто с ног на голову 🇦🇺
Это касается и модификаторов в декларативных UI фреймворках. На картинке видно, что цепочка из одинаковых модификаторов для Compose и SwiftUI дают один и тот же результат, при этом располагаясь в обратном порядке.
#Compose #SwiftUI
@kotlin_adept
Please open Telegram to view this post
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
Пока готовился к докладу, нашел неплохой репозиторий с набором разных анимаций для Compose Multiplatform.
Там вы найдете множество разных примеров:
🟣 Анимации заставок разных приложений (Netflix, Twitter, GitHub, Slack и др.)
🟣 Кастомный pull-to-refresh
🟣 Анимация горения свечи
🟣 Упоротая сова из Duolingo
А если вы iOS разработчик, то вот вам еще более классный репозиторий с кучей красивых анимаций для SwiftUI💅
#Animation #Compose #KMP #SwiftUI
@kotlin_adept
Там вы найдете множество разных примеров:
А если вы iOS разработчик, то вот вам еще более классный репозиторий с кучей красивых анимаций для SwiftUI
#Animation #Compose #KMP #SwiftUI
@kotlin_adept
Please open Telegram to view this post
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
Адаптивный UI проще, чем кажется
Раньше с Android View, если требовалось поддержать верстку для планшетов, довольно часто просто делали отдельную верстку с нуля, и несмотря на то, что можно было расположить несколько фрагментов на одном экране, это не избавляло от сложностей навигации🥲
Теперь же с приходом Compose и нового api делать адаптивную верстку стало значительно проще. И вот несколько рекомендаций как сделать современный адаптивный UI:
1️⃣ Не используйте флаги вроде isTablet и т.д., используйте window size classes для динамического определения размера окна: Compact, Medium, Expanded
2️⃣ Используйте готовые адаптивные компоненты вроде ListDetailPaneScaffold, SupportingPaneScaffold, NavigationSuiteScaffold
3️⃣ Рассмотрите возможность использования LazyGrid вместо LazyList
4️⃣ Меняйте расположение UI компонентов с помощью BoxWithConstraint и movableContentOf во избежание лишних рекомпозиций
5️⃣ Не блокируйте ориентацию экрана и не отключайте resizeableActivity
6️⃣ Меняйте размер и соотношение сторон у UI компонентов в зависимости от размеров окна
🌳 В Decompose также появилась поддержка адаптивной навигации и благодаря ChildPanels реализовать list-detail навигацию стало очень просто без лишнего бойлерплейта.
А есть ли адаптивная верстка в вашем приложении❓
🫡 — только screenOrientation portrait, только хардкор
😎 — есть адаптивная верстка под любые экраны
#Compose #AdaptiveUI
@kotlin_adept
Раньше с Android View, если требовалось поддержать верстку для планшетов, довольно часто просто делали отдельную верстку с нуля, и несмотря на то, что можно было расположить несколько фрагментов на одном экране, это не избавляло от сложностей навигации
Теперь же с приходом Compose и нового api делать адаптивную верстку стало значительно проще. И вот несколько рекомендаций как сделать современный адаптивный UI:
А есть ли адаптивная верстка в вашем приложении
🫡 — только screenOrientation portrait, только хардкор
😎 — есть адаптивная верстка под любые экраны
#Compose #AdaptiveUI
@kotlin_adept
Please open Telegram to view this post
VIEW IN TELEGRAM
Compose Multiplatform в проде
Хочу поделиться новостью: мы выпустили первое приложение, полностью написанное на Compose Multiplatform для iOS😌
Изначально приложение разрабатывалось только для Android, но использовался Kotlin-стек (Decompose, Ktor, SqlDelight, Koin) и обычный Jetpack Compose. Чтобы запустить его в каком-то виде на iOS, потребовалось всего 4 дня! Конечно, доведение до релиза заняло значительно больше времени, но всё равно это оказалось гораздо быстрее, чем полноценная разработка аналогичного проекта с нуля.
Что по итогам:
🟣 Compose в релизной версии вполне прилично работает, особенно на новых устройствах с поддержкой 120 Гц
🟣 Управление жестами удалось легко реализовать благодаря Decompose
🟣 Скролл подлагивает и не ощущается как нативный
🟣 BottomSheet, как всегда причиняет боль 😬
🟣 Есть некоторые баги с TextField
🟣 Некоторые контролы пришлось реализовать нативно, например, WebView, TimePicker и т.д.
Тем не менее, я уверен, что многие проблемы будут исправлены в будущем и уже сейчас Compose Multiplatform можно использовать в проектах, где плавность интерфейса не является критически важной👍
#iOS #Compose
Хочу поделиться новостью: мы выпустили первое приложение, полностью написанное на Compose Multiplatform для iOS
Изначально приложение разрабатывалось только для Android, но использовался Kotlin-стек (Decompose, Ktor, SqlDelight, Koin) и обычный Jetpack Compose. Чтобы запустить его в каком-то виде на iOS, потребовалось всего 4 дня! Конечно, доведение до релиза заняло значительно больше времени, но всё равно это оказалось гораздо быстрее, чем полноценная разработка аналогичного проекта с нуля.
Что по итогам:
Тем не менее, я уверен, что многие проблемы будут исправлены в будущем и уже сейчас Compose Multiplatform можно использовать в проектах, где плавность интерфейса не является критически важной
#iOS #Compose
Please open Telegram to view this post
VIEW IN TELEGRAM
Как подружить Web History и Compose resources
Недавно столкнулся с проблемой: после добавления поддержки Web History в проект с Compose для Web у меня перестали работать ресурсы, причём это происходило только на вложенных экранах.
Изначально я предположил, что проблема связана с настройками веб-сервера, но нет. В Compose для Web ресурсы загружаются по относительному пути. Это означает, что к текущему URL в браузере добавляется путь до ресурсов. Соответственно, если вы находитесь не на главной странице, то по такому пути ресурсы окажутся недоступными🫥
Посмотрел, что пишут в документации, но никаких рекомендаций там не дается на этот счет, что довольно странно. Поправить же проблему удалось следующим образом:
Добавляем этот код в функцию main в сорсете jsMain, и пути до ресурсов снова становятся корректными.
#Compose #JS #WEB
Недавно столкнулся с проблемой: после добавления поддержки Web History в проект с Compose для Web у меня перестали работать ресурсы, причём это происходило только на вложенных экранах.
Изначально я предположил, что проблема связана с настройками веб-сервера, но нет. В Compose для Web ресурсы загружаются по относительному пути. Это означает, что к текущему URL в браузере добавляется путь до ресурсов. Соответственно, если вы находитесь не на главной странице, то по такому пути ресурсы окажутся недоступными
Посмотрел, что пишут в документации, но никаких рекомендаций там не дается на этот счет, что довольно странно. Поправить же проблему удалось следующим образом:
configureWebResources {
resourcePathMapping { path -> "${location.origin}/$path" }
}
Добавляем этот код в функцию main в сорсете jsMain, и пути до ресурсов снова становятся корректными.
#Compose #JS #WEB
Please open Telegram to view this post
VIEW IN TELEGRAM
Я снова принял участие в программном комитете конференции Podlodka Android Crew.
И нам нужна ваша помощь в выборе темы нового сезона. Какая из предложенных тем вам ближе, и вы бы хотели посетить сезон, посвящённый ей?
#️⃣ Compose: 3 года в продакшене
#️⃣ Тесты 360
#️⃣ Карьера Android-разработчика
Заполните, пожалуйста, форму. Среди участников, заполнивших форму, мы разыграем проходку на конференцию.
И нам нужна ваша помощь в выборе темы нового сезона. Какая из предложенных тем вам ближе, и вы бы хотели посетить сезон, посвящённый ей?
Заполните, пожалуйста, форму. Среди участников, заполнивших форму, мы разыграем проходку на конференцию.
Please open Telegram to view this post
VIEW IN TELEGRAM
Пока мы далеко не отошли от темы Bottom Sheet, хочу снова немного побомбить на то, какой API нам предоставили разработчики этого компонента в Material 3.
Я уже как-то поднимал тему декларативного Bottom Sheet, когда решение о том, показывать его или нет, определяется исключительно состоянием. То есть мы показываем шторку, если ассоциированный с ней стейт ≠ null, иначе скрываем.
И казалось бы, в Material 3 сделали именно так: достаточно просто установить значение false в переменной showBottomSheet, чтобы скрыть его. Но тогда это произойдет без анимации сворачивания компонента⚠️
Чтобы это исправить, придется явно вызывать suspend-функцию hide, но делать это каждый раз, мягко говоря, неудобно. Можно попробовать написать свою декларативную обертку, но придется решить несколько проблем:
🔘 Как сохранять контент при анимации скрытия, если стейта уже нет?
🔘 Как запретить перехватывать Bottom Sheet жестом, пока он сворачивается?
И вторая проблема самая неприятная, так как в Bottom Sheet нельзя отключить обработку жестов, пока он скрывается. Но нам обязательно нужно скрыть его, если ассоциированный стейт уже null, иначе получим неконсистентное состояние.
Как ни странно, в SwiftUI таких проблем нет — декларативная обертка пишется буквально в несколько строчек, что можно увидеть на изображении.
Обертку для Bottom Sheet из Material 3, которая отлично подходит для Slot навигации в Decompose, я уже реализовал и чуть позже поделюсь ею с вами, когда обновлю свой пример KMP-проекта.
#Compose #SwiftUI #BottomSheet
Я уже как-то поднимал тему декларативного Bottom Sheet, когда решение о том, показывать его или нет, определяется исключительно состоянием. То есть мы показываем шторку, если ассоциированный с ней стейт ≠ null, иначе скрываем.
И казалось бы, в Material 3 сделали именно так: достаточно просто установить значение false в переменной showBottomSheet, чтобы скрыть его. Но тогда это произойдет без анимации сворачивания компонента
Чтобы это исправить, придется явно вызывать suspend-функцию hide, но делать это каждый раз, мягко говоря, неудобно. Можно попробовать написать свою декларативную обертку, но придется решить несколько проблем:
И вторая проблема самая неприятная, так как в Bottom Sheet нельзя отключить обработку жестов, пока он скрывается. Но нам обязательно нужно скрыть его, если ассоциированный стейт уже null, иначе получим неконсистентное состояние.
Как ни странно, в SwiftUI таких проблем нет — декларативная обертка пишется буквально в несколько строчек, что можно увидеть на изображении.
Обертку для Bottom Sheet из Material 3, которая отлично подходит для Slot навигации в Decompose, я уже реализовал и чуть позже поделюсь ею с вами, когда обновлю свой пример KMP-проекта.
#Compose #SwiftUI #BottomSheet
Please open Telegram to view this post
VIEW IN TELEGRAM
Иногда при работе с TextField нам нужно управлять положением курсора — например, когда мы хотим отредактировать какой-то текст и вставить его в TextField. В таком случае курсор может остаться в начале строки. Для этого у TextField есть перегрузка, которая принимает TextFieldValue. Однако мы не можем использовать Compose-сущности в модулях без Compose.
Теперь посмотрите на код на изображении. Как думаете, в чём здесь проблема?
Колбэк onValueChange зациклится.
Но, несмотря на это, на большинстве устройств всё будет работать нормально: текст будет вводиться, и курсором можно будет управлять. Однако это не касается устройств Huawei — там текст будет вводиться через раз. Сначала я хотел поругать китайцев, но теперь хочу их похвалить: если бы не устройства Huawei, мы бы не отловили эту проблему.
Давайте разберёмся, почему так происходит:
1. При редактировании текста изменяются messageText и lastSelection.
2. Эти состояния объединяются, и изменяется messageTextField.
3. Устанавливается новое значение в TextField.
4. Снова вызывается onValueChange, поскольку TextFieldValue отличается. А отличается он из-за параметра composition, который неявно меняет сам Compose. Мы его не учитываем — из-за этого и происходит бесконечный цикл.
Как это исправить?
Самый логичный вариант — не использовать локальные состояния, а хранить TextFieldValue напрямую в стейте ViewModel. Но если такой возможности нет, можно создать аналогичный класс в бизнес-логике и маппить значения, главное учитывать все параметры TextFieldValue:
#Compose
Теперь посмотрите на код на изображении. Как думаете, в чём здесь проблема?
Но, несмотря на это, на большинстве устройств всё будет работать нормально: текст будет вводиться, и курсором можно будет управлять. Однако это не касается устройств Huawei — там текст будет вводиться через раз. Сначала я хотел поругать китайцев, но теперь хочу их похвалить: если бы не устройства Huawei, мы бы не отловили эту проблему.
Давайте разберёмся, почему так происходит:
1. При редактировании текста изменяются messageText и lastSelection.
2. Эти состояния объединяются, и изменяется messageTextField.
3. Устанавливается новое значение в TextField.
4. Снова вызывается onValueChange, поскольку TextFieldValue отличается. А отличается он из-за параметра composition, который неявно меняет сам Compose. Мы его не учитываем — из-за этого и происходит бесконечный цикл.
Как это исправить?
Самый логичный вариант — не использовать локальные состояния, а хранить TextFieldValue напрямую в стейте ViewModel. Но если такой возможности нет, можно создать аналогичный класс в бизнес-логике и маппить значения, главное учитывать все параметры TextFieldValue:
TextField(
value = messageState.toComposeTextFieldValue(),
onValueChange = { viewModel.onTextChanged(it.toDomainTextFieldValue()) }
)
#Compose
Обычно SwiftUI и Compose очень похожи между собой, и, как правило, стейт для экранов можно формировать одинаково — это очень удобно при работе с KMP.
Но иногда бывают сильные отличия в API, например, PullToRefresh: если в Compose индикатор показывается по изменению состояния, то в SwiftUI — это асинхронная таска🤬
Недавно мы наткнулись на ещё один такой компонент — контекстное меню. На слайде видно, насколько более громоздко выглядит код на Compose, но не все так однозначно. Здесь разница в том, что, опять же, если в Compose меню показывается в зависимости от состояния, то в SwiftUI мы сразу должны знать, какие элементы отображать в этом меню, и нельзя сделать это по клику. Это неудобно, если элементы контекстного меню формируются динамически.
Чтобы исправить это, придётся в стейте сразу хранить словарь и в зависимости от типа ячейки, на которую кликнули, выбирать нужный набор значений.
💬 А какие ошибки допускали вы при формировании стейта экрана?
#Compose #SwiftUI
Но иногда бывают сильные отличия в API, например, PullToRefresh: если в Compose индикатор показывается по изменению состояния, то в SwiftUI — это асинхронная таска
Недавно мы наткнулись на ещё один такой компонент — контекстное меню. На слайде видно, насколько более громоздко выглядит код на Compose, но не все так однозначно. Здесь разница в том, что, опять же, если в Compose меню показывается в зависимости от состояния, то в SwiftUI мы сразу должны знать, какие элементы отображать в этом меню, и нельзя сделать это по клику. Это неудобно, если элементы контекстного меню формируются динамически.
Чтобы исправить это, придётся в стейте сразу хранить словарь и в зависимости от типа ячейки, на которую кликнули, выбирать нужный набор значений.
#Compose #SwiftUI
Please open Telegram to view this post
VIEW IN TELEGRAM
UI-тестирование в Compose 🎨
Если вы хотите реализовать UI-тесты в Compose, то на сегодняшний день есть три основных решения: официальная библиотека от Google и обёртки над ней — Kakao/Kaspresso и Ultron. Про официальное решение сегодня говорить не будем, а рассмотрим поближе другие библиотеки.
Мы уже много лет используем Kaspresso для UI-тестов, но недавно я решил поближе познакомиться с библиотекой Ultron и сравнить оба решения. На самом деле, по функциональности они очень схожи.
🟡 Ultron выглядит довольно заманчиво: у него есть два ключевых преимущества перед конкурентами — при написании тестов на Ultron практически отсутствует какой-либо бойлерплейт-код, а также Ultron поддерживает Compose Multiplatform. Ещё из плюсов можно выделить качественную документацию. Из недостатков я бы отметил довольно небольшое комьюнити у библиотеки, а также, лично для меня, оказалось непривычным отсутствие связей parent-child между PageObject. Подробнее узнать про Ultron можно в недавно опубликованном докладе от автора библиотеки — Алексея Тюрина.
🔘 Kakao/Kaspresso — проверенные временем библиотеки с большим комьюнити, но описание PageObject в Kakao до версии 1.0 требовало довольно много бойлерплейт-кода, особенно при работе с Lazy-списками. Также механизм flaky safety в Kaspresso не совсем хорошо работает с виртуальным временем в Compose-тестах. Об этом подробно рассказал Паша Стрельченко в своём докладе.
💬 А что используете вы для UI-тестирования в Compose? И оправдан ли тренд на уход от дорогих UI-тестов в проекте?
#Compose #UiTesting
Если вы хотите реализовать UI-тесты в Compose, то на сегодняшний день есть три основных решения: официальная библиотека от Google и обёртки над ней — Kakao/Kaspresso и Ultron. Про официальное решение сегодня говорить не будем, а рассмотрим поближе другие библиотеки.
Мы уже много лет используем Kaspresso для UI-тестов, но недавно я решил поближе познакомиться с библиотекой Ultron и сравнить оба решения. На самом деле, по функциональности они очень схожи.
💬 А что используете вы для UI-тестирования в Compose? И оправдан ли тренд на уход от дорогих UI-тестов в проекте?
#Compose #UiTesting
Please open Telegram to view this post
VIEW IN TELEGRAM