Канал для поиска исполнителей для разных задач и организации мини конкурсов
Last updated 2 месяца, 4 недели назад
Новые и перспективные Web3 игры с добычей токенов.
Чат: https://t.me/Crypto_Wolf_Chat
Правила чата смотрите в описании чата.
Все свои вопросы направляйте в чат или главному модератору чата: @Exudna_118
По теме сотрудничества: @Zombini
Last updated 2 месяца, 2 недели назад
Штош. До свидания Android, может ещё встретимся
Где-то лет пять я пользуюсь fixup'ом через IDE, чтобы добавлять изменения к коммитам из истории, и за это время смирился, что эта фича работает странно. Она просто создаёт коммит, но не ребейзит его автоматически, и приходится через интерактивный ребейз двигать коммит и делать fixup руками. Даже в консольном гите удобнее, там можно при ребейзе указать флаг \-\-autosquash
и все коммиты с префиксом fixup!
или squash!
сами посквошатся как надо. Ну то есть явно какой-то баг в IDE, иначе кнопка "Fixup" почти ничем не отличается от "Interactively Rebase from Here".
И вот спустя годы, когда в очередной раз пригорело от поведения fixup, я решил найти этот баг в YouTrack чтобы влепить звезду. Нашел. И оказалось, что можно было не страдать. Просто IDE учитывает настройку гита rebase.autosquash
и если её выставить в true
, всё начинает работать как надо ?
В комментариях к issue можно наблюдать знатное полыхание на тему "а как я об этом должен был узнать?" и я полностью согласен.
Есть ещё одна ловушка с этой фичей. При коммите нужно обязательно нажать "Commit and Rebase", который спрятан внутри выпадающего списка около кнопки "Commit", иначе магии не произойдёт. Про это, кстати, есть отдельная issue.
? TL;DR
Чтобы в IDE не двигать руками коммиты с префиксами fixup!
и squash!
в нужное место при ребейзе, включи настройку rebase.autosquash
:
git config \-\-global rebase.autosquash true
Ого, в Android Gradle Plugin 8.5.0 оказывается наконец завезли поддержку test fixtures, для модулей на Kotlin. Пока что нужно включать экспериментальный флаг.
Release notes для версии 8.5.0 почему-то пустые, так что узнают только те, кому повезло :D
Test fixtures — любые "приспособления" для применения в тестах. Это могут быть тестовые реализации сущностей, генераторы тестовых данных, специфичные для тестового фреймворка вещи и т.д. В Gradle можно применить плагин test-fixtures к модулю и тогда весь код из source set'а с названием testFixtures
будет доступен в тестах этого модуля и для подключения в тесты других модулей. Если у вас есть и Instrumentation, и Unit-тесты, в testFixtures
можно сложить общий код. Как и в тестах, в test fixtures доступны internal-сущности из модуля.
Так вот, раньше в обычных Kotlin проектах test fixtures работали, в Android + Java тоже, а в Android + Kotlin — нет.
Ошибки при тестировании корутин, которые вижу чаще всего на #review (и сам периодически допускаю, хе-хе).
*1️⃣ Не используется runTest*
Structured concurrency гарантирует нам, что родительская корутина дождётся завершения всех дочерних. И наоборот, если родительскую корутину принудительно завершили, дочерние тоже завершатся. runTest
как раз стартует корутину, срок жизни которой ограничен одним тестом, а значит ограничивает срок жизни всех дочерних корутин. При этом он не просто дожидается завершения всех корутин, но и проверяет, что все они завершились без ошибок.
Вот пример, где вместо runTest
используется самопальный скоуп. Мы ожидаем, что тест упадёт, но он будет зелёным:
```
val testScope = CoroutineScope(StandardTestDispatcher())
@Test
fun false positive
() {
testScope.launch { fail("Opps...") }
}
```
Здесь код внутри launch
даже не выполнится потому что мы просто забыли дать ей возможность выполниться (см. п. 4️⃣). Возможна и другая проблема. Если в корутине выполняется долгая операция, тестовая функция может просто не дождаться её выполнения. В итоге ошибка упадёт уже после того как тест окрасился в зелёный, а может вообще уронить тестовый фреймворк во время выполнения другого теста. Этих проблем не будет, если родительская корутина органичена одним тестом.
*2️⃣ Нет возможности подменить CoroutineScope*
В реальности корутина может запускаться не напрямую из теста, а внутри тестируемой сущности с каким-то собственным скоупом и тогда structured concurrency не сработает и мы снова получаем все проблемы из примера выше. Чтобы это пофиксить, нужно давать возможность передавать CoroutineScope
внутрь сущностей через конструктор или параметры функций.
Например, viewModelScope
работает на Main-диспатчере. До недавнего времени единственным вариантом было перед тестом ViewModel
вызывать Dispatchers.setMain(...)
, но теперь это не нужно. В lifecycle-viewmodel 2.8.0 появилась возможность переопределять viewModelScope
через параметр конструктора! Этот вариант более явный и даёт в тестах полный контроль над корутинами внутри ViewModel
.
Если используете подход с переопределением Main-диспатчера, важно не забывать, что оборачивать тест в runTest
всё ещё нужно. Забыть легко потому что все вызовы suspend-функций будут внутри ViewModel
и кажется, что runTest
бесполезен, но это не так. runTest
неявно подтянет переопределённый Dispatchers.Main
и после выполнения проверит, что все корутины на этом диспатчере завершились успешно.
*3️⃣ Неограниченные StateFlow и SharedFlow*
Это горячие потоки, они никогда не завершаются, так что подписка на такой поток внутри теста неизбежно приведёт к падению теста по таймауту. Есть два варианта обхода проблемы:
1. Искусственно ограничить количество элементов, которые хочется поймать из потока. Самый простой способ — использовать операторы take(n)
или first()
, а где-то будет удобнее использовать Turbine.
2. Если мы не знаем ожидаемое количество элементов и просто хотим поймать всё, что прилетело во Flow
за время теста, можно подписаться на Flow
используя backgroundScope
. Это специальный скоуп внутри TestScope
, который завершается вместе с тестовым скоупом, прерывая выполнение дочерних корутин.
4️⃣ Корутине не даётся шанса выполниться
Вызова launch
недостаточно, чтобы запустить корутину, нужно ещё дать ей шанс выполниться. Например, вот тест где корутине такого шанса я не дал:
``
@Test
fun
not launched coroutine`() = runTest {
var result = 0
launch { result = 42 }
// ***❌*** Fails
assertEquals(expected = 42, actual = result)
}
```
Чтобы исправить проблему, нужно вызвать runCurrent
или yield
после старта корутины. Тогда текущая корутина уступит поток другим корутинам.
Другой вариант — использовать UnconfinedTestScheduler
, тогда все корутины будут сразу же запускаться без необходимости пинать шедулер. Это удобно, когда нет необходимости строго контролировать последовательность выполнения корутин.
Полезное:
- Документация к coroutines-test
- Примеры плохих тестов с возможностью запуска
Пишите в комменты с какими проблемами сталкивались при тестировании корутин!
Продолжаем тренировать #насмотренность с inline-классами. В предыдущем посте я обещал рассказать про boxing inline-классов. Но сначала немного поговорим про примитивы.
Один из моих любимых вопросов на собеседовании — есть ли в Kotlin примитивы? Вот в Java есть примитив int
и объект Integer
, а в Kotlin только Int
. На самом деле при компиляции под JVM компилятор сам решает что использовать. Когда возможно, использует примитив, но если тип используется в дженерике или объявлен как nullable, нужен объект и происходит boxing — примитив оборачивается в объект Integer
.
Теперь вернёмся к inline-классам. Подобно примитивам, при работе с inline-классами Kotlin старается избежать лишних обёрток. Чтобы убедиться, что в конкретном случае не происходит boxing, можно посмотреть как код выглядит для JVM через Show Kotlin Bytecode > Decompile
. Случаи, когда происходит boxing, подробно описаны в KEEP. Рекомендую прочитать полностью, а пока сосредоточимся на самых интересных моментах.
*0️⃣ Нуллабельность*
Если нужна нуллабельность, но хочется обойтись без boxing'а, можно создать специальное значение, которое будет заменять null
. Помните Color.Unspecified
в Compose? Это как раз оно. Кстати, недавно подобные специальные значения добавили и inline-классам входящим в состав TextStyle
. В сообщении к коммиту есть бенчмарки показывающие сколько аллокаций удалось на этом сэкономить.
*1️⃣ Создание подтипов*
Когда в Kotlin появились sealed-интерфейсы, я подумал что они будут неплохо комбинироваться с inline-классами. Была мысль завести абстракцию чтобы одинаково работать со строками и текстовыми ресурсами:
```
sealed interface TextValue {
fun get(resources: Respurces): String
@JvmInline
value class Plain(val value: String) : TextValue {
fun get(resources: Resources) = value
}
@JvmInline
value class Res(@StringRes val resId: Int) : TextValue {
fun get(resources: Resources) = resources.getString(resId)
}
}
```
Проблема в том, что тут от inline-классов нет никакого толка. Мы будем использовать интерфейс, а не конкретный inline-класс, так что boxing будет происходить всегда. Компилятор не знает какую именно реализацию интерфейса мы подложим, а значит не может заменить интерфейс на внутренний тип.
Вариантов минимизации boxing'а в подобных случаях как минимум два:
☝️ Если тип внутреннего значения у разных "подтипов" разный, можно хранить общий супертип. Так сделано в Result, внутри лежит Any?
, а чтобы отличать контент от ошибки, создан вспомогательный тип Failure, в который оборачиваются ошибки.
✌️ Можно комбинировать тип и значение. Это хорошо работает с примитивами. Например, в Compose TextUnit
может иметь размерность Sp
и Em
, но это всё один inline-класс. Информация о типе хранится в младших битах Long'а, а значение в следующих за ними битах. Другой пример такого подхода — Duration из stdlib.
*2️⃣ Дженерики*
С использованием inline-класса на позиции дженерика всё понятно — в этом случае без boxing'а не обойтись. Но дженерики указанные у самого inline-класса не влияют на boxing.
Классный пример применения inline-классов с дженериками — безопасные ID. Представим, что у нас есть несколько разных сущностей со строковыми ID и мы хотим чтобы на уровне контракта нельзя было по ID пользователя запросить товар и наоборот. В 2019 году Jake Wharton предложил создавать inline-классы для строгой типизации ID. С тех пор появилась возможность указывать дженерики у inline-классов и теперь можно не плодить отдельные классы-обёртки на каждый тип сущности, достаточно создать один inline-класс с дженериком:
```
@JvmInline
value class Id(val value: String)
data class User(val id: Id)
```
Всем inline-классы в код!
UPD: @senk0n подсказывает, что Romain Guy недавно написал пример как можно в inline-класс запихнуть целую сетку 8х8, каждая ячейка которой может иметь значение 1
или 0
.
Я не умею использовать LLM
Про AI и LLM слышно из каждого утюга и из-за этого есть ощущение лёгкой паники что если ты не включил AI в свою повседневную рутину, то ты на обочине индустрии. Всё, готовься, что тебя заменит машина. Кого ни спроси, все используют LLM на повседневной основе и говорят, что это здорово упрощает им жизнь.
Я не использую LLM не потому что считаю их бесполезными, а потому что не понятно как использовать, какие конкретно задачи на них перекладывать. Обычно прошаренные пользователи говорят общими формулировками, а хочется конкретики: Была задача X
, я бы её делал N
дней, а с AI сделал за M
часов.
Вокруг LLM столько хайпа, что и ожидания высокие, а когда пробуешь что-то сам, результат получается не очень. Может написать решение для задачи с LeetCode — класс, но в работе мне это вряд ли пригодится. Можно использовать для помощи в изучении незнакомой библиотеки — да, но в ответах может быть больше галлюцинаций чем правды, особенно если задача необычная. Можно переложить написание документации или тестов — оказывается, что это не так просто, нужно уметь объяснить машине что ты от неё хочешь, а понимает она всё буквально.
Использовать не получается и это нормально, потому что LLM это инструмент которым нужно уметь пользоваться. Нужно уметь правильно составлять промпты, а чтобы правильно их составлять, нужно просто больше практиковаться. Чтобы упорно раз за разом пытаться использовать, нужно понимать пользу для себя. Тогда само собой выработается чутьё на задачи которые можно переложить на AI, соберётся своя библиотека промптов и набор паттернов, которые помогают добиться желаемого результата.
Чтобы дать пинок этому процессу, я предлагаю тем кто использует LLM поделиться конкретными кейсами когда без них было бы тяжко. Я начну.
Задача: Нужно рядом с полем ввода номера телефона отображать флаг страны.
Есть набор из 260 картинок с флагами стран, у каждого флага название совпадает с названием страны. Нужно по двухбуквенному коду страны получить её флаг. Готовые библиотеки использовать не получится потому что флажки отрисованы в стиле приложения.
У меня получился такой промпт:
```
Я пишу Android-приложение на Kotlin и мне нужно добавить возможность получать картинку флага страны по её двухбуквенному коду.
Я передам на вход список стран. Пример входных данных указан после текста "ПРИМЕР ЗАПРОСА:".
Вывод должен содержать блок кода, формирующий Map, которая будет устанавливать соответствие двухбуквенного кода страны (в соответствии с ISO 3166-1) её флагу. Флаг страны хранится в формате R.drawable.country_flag_* где вместо * указывается название страны. Пример вывода указан после текста "ПРИМЕР ОТВЕТА:".
ПРИМЕР ЗАПРОСА:
- kazakhstan
- russia
ПРИМЕР ОТВЕТА:
val flags = mapOf(
"RU" to R.drawable.country_flag_russia,
"KZ" to R.drawable.country_flag_kazakhstan,
)
```
После этого я порциями по 50 штук отправлял названия файлов с флагами, а ChatGPT выдавал мне маппинг.
*❓ *Почему бы не сделать маппинг скриптом?
Можно ведь искать код страны по справочнику ISO 3166-1. Да, так можно сделать, если мы уверены, что названия стран написаны именно в таком же формате как в справочнике. По факту же так как названия файлов с флагами писали люди, там были опечатки. LLM успешно понимает что signapur
, это на самом деле singapore
.
*❓ *Сколько заняло решение задачи?
Примерно 5 часов. Около двух часов на отладку промпта, и ещё около трёх часов чтобы успокоить паранойю, что маппинги неправильные.
*❓ *Как можно быть уверенным, что маппинг правильный?
Вот что я сделал, чтобы в этом убедиться:
- Проверил, что коды стран действительно везде двухбуквенные и нет дублей
- Выборочно проверил около 15 флагов из разных мест списка
- Написал тест в котором сравнил коды стран их с кодами стран, которые поддерживает libphonenumber. Так поймал, что некоторых флагов не хватает.
Бонус: Для потерянных флагов я попросил LLM сказать что это за страны и отдал список дизайнерам:
Напиши названия стран соответствующие этим кодам: KN, VI, SJ, ..., TA, PM
Если для территории официально используется флаг другой страны, укажи это
Продолжаю рубрику #насмотренность. Сегодня смотрим на inline-классы. На мой взгляд это одна из самый недооценённых фичей Котлина. Подозреваю, что это потому что потребность в inline-классах неочевидна. Попробую это исправить.
Напомню. Inline-класс это обёртка над другим типом. Он должен иметь строго один параметр в основном конструкторе — значение которое мы оборачиваем. При компиляции вместо нашего класса будет подставлено обёрнутое значение. То есть класс есть, а лишних аллокаций нет. Красота!
Минутка духоты: На самом деле обёрнутое значение не всегда будет инлайниться. Принцип работы похож на (un)boxing примитивов. Но это тема для отдельного поста.
1️⃣ Объявление собственных примитивов
Примитивные типы это то, что должно работать быстро. Мы ожидаем, что примитивы передаются по значению, то есть для них не создаётся объект в куче. Звучит как работа для inline-класса, ведь если в него завернуть примитив, то и перформанс получим как у примитива.
За примером далеко ходить не надо — UInt и другие беззнаковые типы в Kotlin это inline-классы. Но вот где действительно разгулялись с примитивами, так это в Jetpack Compose. Цвета раньше передавались либо как Int
в формате 0xAARRGGBB
, либо как класс, теперь же Color это inline-class. Получаем все удобства класса и легковесность примитива. Кроме цветов есть ещё Dp, Size, Offset и т.д. (для типов, состоящих их нескольких свойств, например Size, под капотом используется упаковка данных в один примитив). Так что, если вас как и меня мучил вопрос почему нет ничего страшного в том, что мы на каждой рекомпозиции создаём объекты, это потому что на самом деле никаких объектов нет.
2️⃣ Дешевые enum'ы
Помните совет гугла заменять enum'ы на Int
с аннотацией @IntDef
? Потом Jake Wharton рассказал, что R8 умеет оптимизировать enum'ы и мы выдохнули.
Так вот если всё-таки нужно заменить enum на примитив, inline-классы помогут обойтись без сомнительных конструкций с @IntDef
, которые опираются только на стат. анализ.
Примеры этого подхода, опять же, можно посмотреть в Compose: TextAlign , TextOverflow и LineBreak. Да, к сожалению, при таком подходе в when
нет проверки, что покрыты все ветви, но на этот компромисс я готов пойти.
3️⃣ Обёртки
Наиболее частое применение inline-классов — создание обёрток над другими типами, чтобы:
? Получить более строгую типизацию. Например, чтобы не ошибиться когда у нас много ID-шников одинакового типа у разных сущностей. Или более явно указать в сигнатуре функции, что "этот параметр должен быть не какой угодно строкой, а именно номером телефона". Это работает благодаря важному свойству inline-классов, что их нельзя сравнивать с другими типами. Попытка такого сравнения выкинет ошибку на этапе компиляции.
? Сузить скоуп для утилитарных функций. Вот есть у нас номер карты пользователя в формате строки. Мы хотим делать с этим номером много всякого: форматировать группами по 4 цифры, вывести маскированный номер карты вида *1234
и т.д. Можно сделать кучу экстеншенов на String
, которые будут видны когда нужно и когда не нужно, а можно завести inline-класс и явно определить что с ним можно делать.
? Добавить новые свойства обёртнутой сущности. Добавить @Immutable
ко спискам? Запросто.
Напоследок пример где и типизация важна, и новые свойства добавляются. Помните как в onMeasure нам прилетают Int'ы, с которыми нужно работать исключительно через утилитарный класс MeasureSpec? В Compose вместо этого прилетает один класс Constraints и мы сразу, без похода в документацию, понимаем набор действий, который можно совершать с этим классом. Ну не красота ли?
Канал для поиска исполнителей для разных задач и организации мини конкурсов
Last updated 2 месяца, 4 недели назад
Новые и перспективные Web3 игры с добычей токенов.
Чат: https://t.me/Crypto_Wolf_Chat
Правила чата смотрите в описании чата.
Все свои вопросы направляйте в чат или главному модератору чата: @Exudna_118
По теме сотрудничества: @Zombini
Last updated 2 месяца, 2 недели назад