Kotlin Adept Notes
1.99K subscribers
68 photos
10 videos
114 links
Канал о разработке на Kotlin и обо всем, что с ним связано
По всем вопросам и рекламе: @ajiekcx
Download Telegram
This media is not supported in your browser
VIEW IN TELEGRAM
Compose withAnimation

В 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
🔥10👍2
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
👍12👏3
Кастомные маски для TextField в Compose

Раньше в Android View реализовать маску для номера телефона, не говоря уже про что-то кастомное было далеко не самой простой задачей и люди, чтобы облегчить себе жизнь, использовали сторонние библиотеки, такие как Decoro.

Но теперь с приходом Compose надобность в сторонних решениях практически отпала, ведь реализовать кастомную маску для TextField можно буквально в 40 строк кода 😱, и это возможно благодаря продуманному и простому API интерфейса 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
🔥8👍41😁1
This media is not supported in your browser
VIEW IN TELEGRAM
Коллега из Контура, Евгений Мельцайкин, написал статью о том, как сделать такую кнопку с помощью кастомного Layout в Compose и как оптимизировать ее, чтобы достичь минимального количества рекомпозиций. Приятного чтения 📕

Исходный код можно посмотреть здесь🐱

А вы сможете на взгляд определить какая кнопка сделана оптимально по количеству рекомпозиций?

#Compose
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥37👍2
Скриншот-тестирование в Compose

Google не так давно выкатили свой тулинг для скриншот-тестирования в Compose в экспериментальном режиме и работает он на основе Compose Preview 👀

У меня довольно скпептическое отношение к превью, за все время работы с Compose у меня постоянно были какие-то проблемы с этим механизмом, а с приходом Compose Multiplatform заставить превьюшки работать тот ещё челлендж, более менее дела с превью обстоят только в новой IDE Fleet, но там ещё ворох других проблем.

Так вот, вернёмся к тестированию, я попробовал этот способ и что могу сказать по текущему состоянию тулинга:

👍Официальное решение для скриншот-тестирования
👍Генерация отчёта с диффом изображений
👍Уже относительно работает и можно использовать на свой страх и риск
👍Тесты прогоняются без эмулятора и соответственно прогоняются быстро

👎Названия сгенерированных скриншотов нельзя поменять
👎Нельзя выборочно обновить эталонный скриншот
👎Нужно использовать специальные gradle таски для валидации скриншотов, потребуются доработки на CI
👎Нельзя настроить минимальный порог отличий между скриншотами
👎Только для Android

Так что подводя итоги, круто, что появляется решение из коробки, но в текущем состоянии завязываться на него довольно опасно.

#Compose #SnapshotTesting
Please open Telegram to view this post
VIEW IN TELEGRAM
11👍8
Нашли серьезную уязвимость в Jetpack Navigation Compose, которая позволяет открыть любой экран в приложении, даже если там нет явных диплинков ⚠️

Эксплуатируется она максимально просто, достаточно знать имя пакета и название маршрута в графе навигации:


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
😁13👍6🔥5😱4👀2👻1
Перевернутые модификаторы

Неудивительно, что Android и iOS разработчики часто не могут найти общий язык, ведь у них (у нас) все перевернуто с ног на голову 🇦🇺

Это касается и модификаторов в декларативных UI фреймворках. На картинке видно, что цепочка из одинаковых модификаторов для Compose и SwiftUI дают один и тот же результат, при этом располагаясь в обратном порядке.

➡️ В Compose первый модификатор size задает минимальные и максимальные констрейнты и мы не можем выйти за эти ограничения, не переопределяя их.

➡️ В SwiftUI таких ограничений нет и там всегда padding применяется во вне, что может быть даже удобнее, так как не приходится об этом задумываться.

🗓 Но к чему я это все? На ближайшей конференции Мобиус буду рассказывать доклад, где сравню ключевые отличия обоих фреймворков, и если тема интересна, то буду рад видеть всех на докладе 😉

#Compose #SwiftUI
@kotlin_adept
Please open Telegram to view this post
VIEW IN TELEGRAM
👍23🔥91😁1🤪1
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
Please open Telegram to view this post
VIEW IN TELEGRAM
👍25😍1
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
Please open Telegram to view this post
VIEW IN TELEGRAM
🫡55😎6👍5❤‍🔥21
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
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥55👍5👏2🤔21
Как подружить Web History и Compose resources

Недавно столкнулся с проблемой: после добавления поддержки 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
👍22😨4
Я снова принял участие в программном комитете конференции Podlodka Android Crew.

И нам нужна ваша помощь в выборе темы нового сезона. Какая из предложенных тем вам ближе, и вы бы хотели посетить сезон, посвящённый ей?

#️⃣Compose: 3 года в продакшене
#️⃣Тесты 360
#️⃣Карьера Android-разработчика

Заполните, пожалуйста, форму. Среди участников, заполнивших форму, мы разыграем проходку на конференцию.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍18
Пока мы далеко не отошли от темы 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
Please open Telegram to view this post
VIEW IN TELEGRAM
👍27😢7💯5
Иногда при работе с TextField нам нужно управлять положением курсора — например, когда мы хотим отредактировать какой-то текст и вставить его в TextField. В таком случае курсор может остаться в начале строки. Для этого у TextField есть перегрузка, которая принимает TextFieldValue. Однако мы не можем использовать Compose-сущности в модулях без Compose.

Теперь посмотрите на код на изображении. Как думаете, в чём здесь проблема?

Колбэк onValueChange зациклится.

Но, несмотря на это, на большинстве устройств всё будет работать нормально: текст будет вводиться, и курсором можно будет управлять. Однако это не касается устройств 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
🔥29
Обычно SwiftUI и Compose очень похожи между собой, и, как правило, стейт для экранов можно формировать одинаково — это очень удобно при работе с KMP.

Но иногда бывают сильные отличия в API, например, PullToRefresh: если в Compose индикатор показывается по изменению состояния, то в SwiftUI — это асинхронная таска 🤬

Недавно мы наткнулись на ещё один такой компонент — контекстное меню. На слайде видно, насколько более громоздко выглядит код на Compose, но не все так однозначно. Здесь разница в том, что, опять же, если в Compose меню показывается в зависимости от состояния, то в SwiftUI мы сразу должны знать, какие элементы отображать в этом меню, и нельзя сделать это по клику. Это неудобно, если элементы контекстного меню формируются динамически.

Чтобы исправить это, придётся в стейте сразу хранить словарь и в зависимости от типа ячейки, на которую кликнули, выбирать нужный набор значений.

💬 А какие ошибки допускали вы при формировании стейта экрана?

#Compose #SwiftUI
Please open Telegram to view this post
VIEW IN TELEGRAM
👍20🔥3
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
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17
Как встроить SwiftUI в Compose Multiplatform

Обычно я стараюсь избегать использования кастомных CompositionLocal в Compose, так как это добавляет неявные зависимости, и если не предоставить значение, приложение упадёт в рантайме. Я придерживаюсь подхода, в котором CompositionLocal можно использовать только тогда, когда значение действительно может быть полезно любой Composable-функции в дереве. Яркий пример — тема приложения.

И при работе с Compose Multiplatform я подсмотрел классное применение этого механизма для встраивания SwiftUI вьюшек в Composable функции.

1. В сорсете iosMain создаём CompositionLocal и интерфейс NativeViewFactory.
2. На стороне Swift реализуем этот интерфейс и передаём его в функцию создания UIViewController.
3. В этой функции пробрасываем фабрику через CompositionLocalProvider.
4. Далее в любом месте поддерева в iosMain можно получить доступ к этой нативной вьюшке.

🌐 Посмотреть пример приложения для сканирования QR-кодов с этим подходом можно в репозитории, который я подготовил для лекции в онлайн-университете.

#Compose #SwiftUI
Please open Telegram to view this post
VIEW IN TELEGRAM
❤‍🔥10🔥5👍21
Make Snackbar Great Again

В Compose реализация показа snackbar получилась не самой удобной, на каждом экране приходится:
🔘 Оборачивать контент в Scaffold
🔘 Вызывать suspend-функции для показа snackbar
🔘 Не забывать предусмотреть отмену предыдущего snackbar
🔘 Страдать из-за BottomSheet, так как там не получится использовать Scaffold, иначе он растянется на весь экран

И даже если сделать один глобальный обработчик, snackbar будет залезать на BottomNavigation, что выглядит довольно плохо 🤔

Вот что я предлагаю:
🟢Отказаться от показа snackbar через Scaffold и написать простую логику с декларативным отображением snackbar с анимацией в зависимости от состояния.
🟢Чтобы snackbar не перекрывал лишние элементы интерфейса, можно написать кастомный модификатор в Compose, который будет запоминать верхнюю позицию элемента, к которому он применён, и добавлять неявный отступ к snackbar.

Таким образом, эта реализация не только решает проблемы выше, но и позволяет отображать snackbar как внизу, так и вверху экрана.

🔥 Если тема интересна, ставьте реакции и я попробую собрать пример с такой реализацией.

#Compose
Please open Telegram to view this post
VIEW IN TELEGRAM
1🔥121🥴9👍84
📣 По вашим заявкам сделал небольшую библиотеку для декларативного показа снекбаров.

Преимущества библиотеки:
🟢 Полностью декларативный API, забудьте про one-time события и их обработку в LaunchedEffect.
🟢 Можно сделать как один глобальный обработчик, так и множество вложенных.
🟢 Благодаря встроенным модификаторам важный UI не будет перекрываться снекбаром.
🟢 Можно отображать снекбар как внизу, так и вверху экрана.
🟢 Поддерживаются любые типы сообщений, а не только строки.
🟢 Нет зависимости от Material, отображайте любой UI, какой захотите.
🟢 Настраиваемые анимации появления и скрытия снекбара.
🟢 Поддерживаются Android, iOS и JVM-таргеты.
🟢 Простая интеграция с Decompose.

🌐 Инструкцию по подключению, сэмпл и исходный код смотрите в репозитории.

Звёздочки на репозиторий и репосты приветствуются!

#Compose #Snackbar #KMP
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥68👍94🤔1
Как слить кодовую базу из-за Compose Multiplatform

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

Если в Android мы переживаем за то, чтобы никто не зареверсил наш код, подключаем R8 и так далее, то как обеспечить такой же уровень защиты в вебе?

Казалось бы, там всё ок из коробки: в релизном билде что-то понять из итогового JS-файла не получится, особенно с Wasm.

Но есть один маленький нюанс: по умолчанию в настройках Webpack исходный код тоже публикуется, и его любой может успешно посмотреть в Developer Tools. А в документации по Compose Multiplatform нет ни одного упоминания о том, как это предотвратить 🤡

Так что обязательно отключайте публикацию сорсов в настройках Webpack на релизе. Как это сделать — смотрите в комментариях 👇

#Security #Compose #WEB
Please open Telegram to view this post
VIEW IN TELEGRAM
28👍18🤬4🔥2