Я не умею использовать LLM
Про AI и LLM слышно из каждого утюга и из-за этого есть ощущение лёгкой паники что если ты не включил AI в свою повседневную рутину, то ты на обочине индустрии. Всё, готовься, что тебя заменит машина. Кого ни спроси, все используют LLM на повседневной основе и говорят, что это здорово упрощает им жизнь.
Я не использую LLM не потому что считаю их бесполезными, а потому что не понятно как использовать, какие конкретно задачи на них перекладывать. Обычно прошаренные пользователи говорят общими формулировками, а хочется конкретики: Была задача
Вокруг LLM столько хайпа, что и ожидания высокие, а когда пробуешь что-то сам, результат получается не очень. Может написать решение для задачи с LeetCode — класс, но в работе мне это вряд ли пригодится. Можно использовать для помощи в изучении незнакомой библиотеки — да, но в ответах может быть больше галлюцинаций чем правды, особенно если задача необычная. Можно переложить написание документации или тестов — оказывается, что это не так просто, нужно уметь объяснить машине что ты от неё хочешь, а понимает она всё буквально.
Использовать не получается и это нормально, потому что LLM это инструмент которым нужно уметь пользоваться. Нужно уметь правильно составлять промпты, а чтобы правильно их составлять, нужно просто больше практиковаться. Чтобы упорно раз за разом пытаться использовать, нужно понимать пользу для себя. Тогда само собой выработается чутьё на задачи которые можно переложить на AI, соберётся своя библиотека промптов и набор паттернов, которые помогают добиться желаемого результата.
Чтобы дать пинок этому процессу, я предлагаю тем кто использует LLM поделиться конкретными кейсами когда без них было бы тяжко. Я начну.
Задача: Нужно рядом с полем ввода номера телефона отображать флаг страны.
Есть набор из 260 картинок с флагами стран, у каждого флага название совпадает с названием страны. Нужно по двухбуквенному коду страны получить её флаг. Готовые библиотеки использовать не получится потому что флажки отрисованы в стиле приложения.
У меня получился такой промпт:
После этого я порциями по 50 штук отправлял названия файлов с флагами, а ChatGPT выдавал мне маппинг.
❓ Почему бы не сделать маппинг скриптом?
Можно ведь искать код страны по справочнику ISO 3166-1. Да, так можно сделать, если мы уверены, что названия стран написаны именно в таком же формате как в справочнике. По факту же так как названия файлов с флагами писали люди, там были опечатки. LLM успешно понимает что
❓ Сколько заняло решение задачи?
Примерно 5 часов. Около двух часов на отладку промпта, и ещё около трёх часов чтобы успокоить паранойю, что маппинги неправильные.
❓ Как можно быть уверенным, что маппинг правильный?
Вот что я сделал, чтобы в этом убедиться:
- Проверил, что коды стран действительно везде двухбуквенные и нет дублей
- Выборочно проверил около 15 флагов из разных мест списка
- Написал тест в котором сравнил коды стран их с кодами стран, которые поддерживает libphonenumber. Так поймал, что некоторых флагов не хватает.
Бонус: Для потерянных флагов я попросил LLM сказать что это за страны и отдал список дизайнерам:
#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
Если для территории официально используется флаг другой страны, укажи это
#llm
Please open Telegram to view this post
VIEW IN TELEGRAM
Отрадно видеть, что канал Android Good Reads восстал из пепла стараниями @Anton Kondratiuk и теперь регулярно постит годные материалы. Однозначно рекомендация к подписке :D
Telegram
Android Good Reads
Самые интересные статьи, видео и новости, связанные с Android разработкой. Не больше трёх материалов в день.
Автор канала: @Lamprof
Размещение рекламы: @tanyasanovna
Автор канала: @Lamprof
Размещение рекламы: @tanyasanovna
Продолжаем тренировать #насмотренность с inline-классами. В предыдущем посте я обещал рассказать про boxing inline-классов. Но сначала немного поговорим про примитивы.
Один из моих любимых вопросов на собеседовании — есть ли в Kotlin примитивы? Вот в Java есть примитив
Теперь вернёмся к inline-классам. Подобно примитивам, при работе с inline-классами Kotlin старается избежать лишних обёрток. Чтобы убедиться, что в конкретном случае не происходит boxing, можно посмотреть как код выглядит для JVM через
0️⃣ Нуллабельность
Если нужна нуллабельность, но хочется обойтись без boxing'а, можно создать специальное значение, которое будет заменять
1️⃣ Создание подтипов
Когда в Kotlin появились sealed-интерфейсы, я подумал что они будут неплохо комбинироваться с inline-классами. Была мысль завести абстракцию чтобы одинаково работать со строками и текстовыми ресурсами:
Проблема в том, что тут от inline-классов нет никакого толка. Мы будем использовать интерфейс, а не конкретный inline-класс, так что boxing будет происходить всегда. Компилятор не знает какую именно реализацию интерфейса мы подложим, а значит не может заменить интерфейс на внутренний тип.
Вариантов минимизации boxing'а в подобных случаях как минимум два:
☝️ Если тип внутреннего значения у разных "подтипов" разный, можно хранить общий супертип. Так сделано в Result, внутри лежит
✌️ Можно комбинировать тип и значение. Это хорошо работает с примитивами. Например, в Compose
2️⃣ Дженерики
С использованием inline-класса на позиции дженерика всё понятно — в этом случае без boxing'а не обойтись. Но дженерики указанные у самого inline-класса не влияют на boxing.
Классный пример применения inline-классов с дженериками — безопасные ID. Представим, что у нас есть несколько разных сущностей со строковыми ID и мы хотим чтобы на уровне контракта нельзя было по ID пользователя запросить товар и наоборот. В 2019 году Jake Wharton предложил создавать inline-классы для строгой типизации ID. С тех пор появилась возможность указывать дженерики у inline-классов и теперь можно не плодить отдельные классы-обёртки на каждый тип сущности, достаточно создать один inline-класс с дженериком:
Всем inline-классы в код!
UPD: @senk0n подсказывает, что Romain Guy недавно написал пример как можно в inline-класс запихнуть целую сетку 8х8, каждая ячейка которой может иметь значение
Один из моих любимых вопросов на собеседовании — есть ли в 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<out T>(val value: String)
data class User(val id: Id<User>)
Всем inline-классы в код!
UPD: @senk0n подсказывает, что Romain Guy недавно написал пример как можно в inline-класс запихнуть целую сетку 8х8, каждая ячейка которой может иметь значение
1
или 0
.Есть у меня традиция раз в год делать большое обновление библиотеки шифрования DataStore'ов.
В этот раз в версии 1.0.0 упростил создание зашифрованных сторов за счёт использования делегата:
А в версии 1.1.1-beta01 смигрировал либу на новенький
В общем-то на этом можно было бы считать долг мейнтейнера выполненным на год вперёд, если бы не приколы от Google.
🪦 security-crypto всё.
Да. Внезапно выяснилось, что security-crypto теперь deprecated.
Никаких анонсов с объяснениями этого решения я не нашёл, да и узнать об этом изменении можно только из одной строчки в документации по криптографии или если посмотреть на последний коммит в пакете security-crypto, где всё содержимое библиотеки просто помечается как deprecated.
Что дальше?
Для encrypted-datastore это не критично, так как security-crypto была нужна только для удобства. Всегда можно вернуться к получению
А вот что делать тем кто использует
Есть и плюс. Это была единственная Android-специфичная библиотека, которая мешала сделать encrypted-datastore мультиплатформенной библиотекой. Если от неё избавиться, останется только собрать KMP-либу из разных реализаций Tink.
#datastore #security
В этот раз в версии 1.0.0 упростил создание зашифрованных сторов за счёт использования делегата:
// Before
val dataStore = DataStoreFactory.createEncrypted(serializer) {
EncryptedFile.Builder(
context.dataStoreFile("filename"),
context,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}
// After
val Context.dataStore by encryptedDataStore(
"filename",
serializer,
)
А в версии 1.1.1-beta01 смигрировал либу на новенький
datastore:1.1.1
.В общем-то на этом можно было бы считать долг мейнтейнера выполненным на год вперёд, если бы не приколы от Google.
🪦 security-crypto всё.
Да. Внезапно выяснилось, что security-crypto теперь deprecated.
Никаких анонсов с объяснениями этого решения я не нашёл, да и узнать об этом изменении можно только из одной строчки в документации по криптографии или если посмотреть на последний коммит в пакете security-crypto, где всё содержимое библиотеки просто помечается как deprecated.
Что дальше?
Для encrypted-datastore это не критично, так как security-crypto была нужна только для удобства. Всегда можно вернуться к получению
StreamingAead
через Tink. Конечно, это повлечёт за собой очередную миграцию 🙈 но я попробую сделать это максимально безболезненным процессом.А вот что делать тем кто использует
EncryptedSharedPreferences
? В сообщении депрекейта написано "Use SharedPreferences instead". Так себе альтернатива. Точно не лишним будет начать искать куда переносить данные из шифрованных префов.Есть и плюс. Это была единственная Android-специфичная библиотека, которая мешала сделать encrypted-datastore мультиплатформенной библиотекой. Если от неё избавиться, останется только собрать KMP-либу из разных реализаций Tink.
#datastore #security
Ошибки при тестировании корутин, которые вижу чаще всего на #review (и сам периодически допускаю, хе-хе).
1️⃣ Не используется runTest
Structured concurrency гарантирует нам, что родительская корутина дождётся завершения всех дочерних. И наоборот, если родительскую корутину принудительно завершили, дочерние тоже завершатся.
Вот пример, где вместо
Здесь код внутри
2️⃣ Нет возможности подменить CoroutineScope
В реальности корутина может запускаться не напрямую из теста, а внутри тестируемой сущности с каким-то собственным скоупом и тогда structured concurrency не сработает и мы снова получаем все проблемы из примера выше. Чтобы это пофиксить, нужно давать возможность передавать
Например,
Если используете подход с переопределением Main-диспатчера, важно не забывать, что оборачивать тест в
3️⃣ Неограниченные StateFlow и SharedFlow
Это горячие потоки, они никогда не завершаются, так что подписка на такой поток внутри теста неизбежно приведёт к падению теста по таймауту. Есть два варианта обхода проблемы:
1. Искусственно ограничить количество элементов, которые хочется поймать из потока. Самый простой способ — использовать операторы
2. Если мы не знаем ожидаемое количество элементов и просто хотим поймать всё, что прилетело во
4️⃣ Корутине не даётся шанса выполниться
Вызова
Чтобы исправить проблему, нужно вызвать
Другой вариант — использовать
Полезное:
- Документация к coroutines-test
- Примеры плохих тестов с возможностью запуска
Пишите в комменты с какими проблемами сталкивались при тестировании корутин!
#test #coroutines
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
- Примеры плохих тестов с возможностью запуска
Пишите в комменты с какими проблемами сталкивались при тестировании корутин!
#test #coroutines
На выходных посмотрел доклад про Declarative Gradle и демку Amper от JetBrains с KotlinConf'24. Вообще, когда только услышал про эти два инструмента, было много вопросов, главные из которых: "Мы только Gradle готовить научились, зачем что-то менять?", "Уже пора переезжать? Если нет, то как подготовиться?" и, конечно, "Какое решение выбрать?". После прочтения доки и просмотра докладов, я нашёл для себя ответы на эти вопросы.
Зачем что-то менять?
Оба проекта направлены на разделение конфигурации сборки (декларативная часть) и логики сборки (императивная часть). Конфигурация остаётся в модулях, а декларативная часть переезжает в плагины. Аргументы такие:
• Для декларативных конфигов проще писать тулинг. Помните случаи когда IDE предлагает добавить или обновить какую-то зависимость? Правда нажимать на эту кнопку не хочется, потому что этот автоматический рефакторинг не знает ничего про специфику проекта и в лучшем случае обновит версию не там, а в худшем сломает сборку. Поддержать в тулинге все возможные варианты объявления зависимостей — нетривиальная задача, которая решается очень просто для декларативных конфигов.
• Ещё один аспект интеграции с тулингом — декларативные конфиги можно парсить "на лету", без компиляции. Открываются возможности для оптимизации. Команда Gradle показала, запуск
• Человеку тоже проще читать и понимать декларативные конфиги. Возможно это субъективно, но сложнее понимать что происходит в билд скриптах если логика перемешана с конфигами, так же сложно воспринимается код на Compose где логика не вынесена во
• С декларативным конфигом сложнее "выстрелить себе в ногу", сложнее сделать сборку неэффективной. Просто потому что набор инструментов для этого ограничен. Конечно, такая возможность всё ещё остаётся в декларативной части. Все эти знания про "configuration cache", "configuration avoidance" и т.д. просто не будут нужны для грамотной настройки проекта, но всё ещё пригодятся если хочется написать свой плагин.
Что нам делать сейчас?
Оба проекта в экспериментальном статусе, а значит ещё будут значительные изменения, поэтому в прод тащить пока рано. Но уже можно экспериментировать на небольших проектах или попробовать смигрировать один простой модуль на новый вариант конфигурации (даже не обязательно эту ветку потом вливать). Благо оба инструмента можно использовать поверх сконфигурированного Gradle-проекта. Конечно, в результате экспериментов хорошо бы собрать список пожеланий и отправить их командам.
Если на эксперименты нет времени, можно упростить себе жизнь сейчас и миграцию на декларативный подход в будущем, если приблизить конфигурацию сборки к декларативной. Для этого нужно перенести всю логику в precompiled script plugins или обычные плагины, а в скриптах сборки оставить только настройку этих плагинов.
Какое решение выбрать?
Ответ на этот вопрос будет понятен позднее, но уже видны некоторые отличия. Amper строится с Kotlin Multiplatform в уме, а Declarative Gradle всегда будет лучше интегрирован с Gradle. У Amper больше свободы в решениях, так как он не зависит от Gradle. Как пример, изменение структуры проекта с
В общем, следим как будут развиваться эти инструменты. Похоже, что в будущем конфиги сборки будут всё-таки декларативными. Интересно как будет соблюдаться баланс между ограничениями декларативной конфигурации и свободой, которую даёт Gradle.
По теме:
- Репозиторий Amper
- Репозиторий Declarative Gradle
- Why Declarative Gradle is a cool thing I am afraid of: Maven strikes back - краткая история систем сборки и другая точка зрения
#gradle #amper
Зачем что-то менять?
Оба проекта направлены на разделение конфигурации сборки (декларативная часть) и логики сборки (императивная часть). Конфигурация остаётся в модулях, а декларативная часть переезжает в плагины. Аргументы такие:
• Для декларативных конфигов проще писать тулинг. Помните случаи когда IDE предлагает добавить или обновить какую-то зависимость? Правда нажимать на эту кнопку не хочется, потому что этот автоматический рефакторинг не знает ничего про специфику проекта и в лучшем случае обновит версию не там, а в худшем сломает сборку. Поддержать в тулинге все возможные варианты объявления зависимостей — нетривиальная задача, которая решается очень просто для декларативных конфигов.
• Ещё один аспект интеграции с тулингом — декларативные конфиги можно парсить "на лету", без компиляции. Открываются возможности для оптимизации. Команда Gradle показала, запуск
./gradlew assemble
на проекте с 500 модулями и вариант с декларативной конфигурацией занял 11 секунд против 72 секунд с Kotlin DSL. Скорее всего это какой-то супер простой не-Android проект и, к тому же, они сами говорят, что когда фичей в декларативном варианте будет больше, будет уже не так быстро, но всё равно впечатляет. Стоит воспринимать этот результат как экономию именно на этапе конфигурации.• Человеку тоже проще читать и понимать декларативные конфиги. Возможно это субъективно, но сложнее понимать что происходит в билд скриптах если логика перемешана с конфигами, так же сложно воспринимается код на Compose где логика не вынесена во
ViewModel
.• С декларативным конфигом сложнее "выстрелить себе в ногу", сложнее сделать сборку неэффективной. Просто потому что набор инструментов для этого ограничен. Конечно, такая возможность всё ещё остаётся в декларативной части. Все эти знания про "configuration cache", "configuration avoidance" и т.д. просто не будут нужны для грамотной настройки проекта, но всё ещё пригодятся если хочется написать свой плагин.
Что нам делать сейчас?
Оба проекта в экспериментальном статусе, а значит ещё будут значительные изменения, поэтому в прод тащить пока рано. Но уже можно экспериментировать на небольших проектах или попробовать смигрировать один простой модуль на новый вариант конфигурации (даже не обязательно эту ветку потом вливать). Благо оба инструмента можно использовать поверх сконфигурированного Gradle-проекта. Конечно, в результате экспериментов хорошо бы собрать список пожеланий и отправить их командам.
Если на эксперименты нет времени, можно упростить себе жизнь сейчас и миграцию на декларативный подход в будущем, если приблизить конфигурацию сборки к декларативной. Для этого нужно перенести всю логику в precompiled script plugins или обычные плагины, а в скриптах сборки оставить только настройку этих плагинов.
Какое решение выбрать?
Ответ на этот вопрос будет понятен позднее, но уже видны некоторые отличия. Amper строится с Kotlin Multiplatform в уме, а Declarative Gradle всегда будет лучше интегрирован с Gradle. У Amper больше свободы в решениях, так как он не зависит от Gradle. Как пример, изменение структуры проекта с
src/main/kotlin/
на src/
. Standalone вариант Amper может дать ещё больше свободы и более тесную интеграцию с IDE.В общем, следим как будут развиваться эти инструменты. Похоже, что в будущем конфиги сборки будут всё-таки декларативными. Интересно как будет соблюдаться баланс между ограничениями декларативной конфигурации и свободой, которую даёт Gradle.
По теме:
- Репозиторий Amper
- Репозиторий Declarative Gradle
- Why Declarative Gradle is a cool thing I am afraid of: Maven strikes back - краткая история систем сборки и другая точка зрения
#gradle #amper
Что выведет этот код?
Anonymous Quiz
6%
bar(x=42, y=0)
1%
bar(x=42, y=0.0)
22%
bar(x=42)
26%
bar()
45%
Не скомпилируется: "Overload resolution ambiguity"
Ого, в Android Gradle Plugin 8.5.0 оказывается наконец завезли поддержку test fixtures, для модулей на Kotlin. Пока что нужно включать экспериментальный флаг.
Release notes для версии 8.5.0 почему-то пустые, так что узнают только те, кому повезло :D
Test fixtures — любые "приспособления" для применения в тестах. Это могут быть тестовые реализации сущностей, генераторы тестовых данных, специфичные для тестового фреймворка вещи и т.д. В Gradle можно применить плагин test-fixtures к модулю и тогда весь код из source set'а с названием
Так вот, раньше в обычных Kotlin проектах test fixtures работали, в Android + Java тоже, а в Android + Kotlin — нет.
#agp
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 — нет.
#agp
Где-то лет пять я пользуюсь fixup'ом через IDE, чтобы добавлять изменения к коммитам из истории, и за это время смирился, что эта фича работает странно. Она просто создаёт коммит, но не ребейзит его автоматически, и приходится через интерактивный ребейз двигать коммит и делать fixup руками. Даже в консольном гите удобнее, там можно при ребейзе указать флаг
И вот спустя годы, когда в очередной раз пригорело от поведения fixup, я решил найти этот баг в YouTrack чтобы влепить звезду. Нашел. И оказалось, что можно было не страдать. Просто IDE учитывает настройку гита
В комментариях к issue можно наблюдать знатное полыхание на тему "а как я об этом должен был узнать?" и я полностью согласен.
Есть ещё одна ловушка с этой фичей. При коммите нужно обязательно нажать "Commit and Rebase", который спрятан внутри выпадающего списка около кнопки "Commit", иначе магии не произойдёт. Про это, кстати, есть отдельная issue.
👆 TL;DR
Чтобы в IDE не двигать руками коммиты с префиксами
#git #idea
--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
#git #idea
Штош. До свидания Android, может ещё встретимся
Андрей Бреслав, Роман Елизаров, Дмитрий Жемеров, Светлана Исакова и другие причастные к созданию языка полтора часа рассказывают как это было. Интересно, что заход Kotlin в Android по сути был случайным и мобильные разработчики вообще не были целевой аудиторией :)
(Да, я знаю, что это Telegram, а не Twitter :D)
Please open Telegram to view this post
VIEW IN TELEGRAM
YouTube
Beyond The Success Of Kotlin / The Documentary by EngX
Almost 15 years ago, a small engineering team at JetBrains embarked on what seemed like a crazy endeavor — to create their own programming language and succeed with it. In the early 2010s, Java was one of the most popular languages, used by millions of engineers.…
Я считаю, что в релизной версии приложения нужно оставлять логи, и сейчас я вам докажу это на примере из жизни.
Недавно я стал клиентом Generali Srbija (страховая компания) и пользователем их замечательного мобильного приложения. Зарегистрировался, авторизовался и... приложение моментально закрывается когда я его открываю.
Первая догадка — возможно приложению не понравилось, что я отклонил все запросы пермишенов, включая доступ к микрофону, совершению звонков и контактам. Это оказалось ни при чём. Тогда я решил проверить есть ли что-то полезное в logcat (см. скриншот). Логи, которые выводят мой токен, хэш пароля и персональные данные, а так же сообщения типа "Я тут" пропускаем. В конце видим, что приложение само вызывает
Дальше остаётся только декомпилировать APK через APKTool и поиском найти нужное сообщение лога. Отгадка оказалась простой — если у пользователя обнаружен root, приложение просто закрывается. Кто-то скажет, что это не user-friendly решение, но я парирую цитатой одного мудрого разработчика: "Пользователь всё равно найдёт как сломать приложение, поэтому мы решили не обрабатывать краевые сценарии".
А теперь представьте как было бы сложно понять что происходит, если бы не было никаких логов, приложение бы закрывалось не через
Ещё один вывод — если вы ищете работу в Сербии, Generali Srbija отчаянно нуждаются в разработчиках, просто пока этого не поняли.
#security
Недавно я стал клиентом Generali Srbija (страховая компания) и пользователем их замечательного мобильного приложения. Зарегистрировался, авторизовался и... приложение моментально закрывается когда я его открываю.
Первая догадка — возможно приложению не понравилось, что я отклонил все запросы пермишенов, включая доступ к микрофону, совершению звонков и контактам. Это оказалось ни при чём. Тогда я решил проверить есть ли что-то полезное в logcat (см. скриншот). Логи, которые выводят мой токен, хэш пароля и персональные данные, а так же сообщения типа "Я тут" пропускаем. В конце видим, что приложение само вызывает
System.exit(0)
, написав перед этим "Это настоящая загрузка".Дальше остаётся только декомпилировать APK через APKTool и поиском найти нужное сообщение лога. Отгадка оказалась простой — если у пользователя обнаружен root, приложение просто закрывается. Кто-то скажет, что это не user-friendly решение, но я парирую цитатой одного мудрого разработчика: "Пользователь всё равно найдёт как сломать приложение, поэтому мы решили не обрабатывать краевые сценарии".
А теперь представьте как было бы сложно понять что происходит, если бы не было никаких логов, приложение бы закрывалось не через
System.exit
и была включена обфускация! Поэтому если вы не уверены в стабильности приложения, оставляйте логи и отключайте обфускацию. Так пользователи смогут сами подебажить приложение и отправить вам на почту результаты своего исследования, останется только пофискить. Можно было бы ещё флаг debuggable=true
выставить, но Google Play такое не пропустит ☹️Ещё один вывод — если вы ищете работу в Сербии, Generali Srbija отчаянно нуждаются в разработчиках, просто пока этого не поняли.
#security
В общем, я ВНЕЗАПНО понял, что уже вообще-то почти декабрь, а значить вот-вот начнётся новый Advent of Code. Забираю обратно все свои подтрунивания над коммунальными службами, которые "не были готовы к зиме". Я тоже не был готов.
Для новых подписчиков – я уже третий год организую "клуб решал Advent of Code" и мы 25 дней решаем задачи истрадаем получаем положительные эмоции. В этом году организацию начал очень поздно, так что скорее всего состав решающих будет достаточно камерный :)
Заходите в канал @aoc_club и зовите друзей!
Для новых подписчиков – я уже третий год организую "клуб решал Advent of Code" и мы 25 дней решаем задачи и
Заходите в канал @aoc_club и зовите друзей!
Forwarded from Advent of Code Club (Osip Fatkullin)
Please open Telegram to view this post
VIEW IN TELEGRAM
Этого ещё никто не видел, вы первые 👀
Теперь принятие архитектурных решени и проектирование нового функционала для Ktor происходит публично в репозитории ktor-klip!
Первый KLIP на очереди – официальное решение для DI. Все желающие могут посмотреть какой планируется дизайн и повлиять на него на ранней стадии!
Если вы используете Ktor в качестве клиента, эти изменения пока никак вас не коснутся, они направлены на то чтобы упростить использование DI при написании серверов.
А здесь в комментариях можно похоливарить про "зачем ещё один DI?", "почему не Koin?", "будет compile-time валидация графа?". Хотя если прочитать документ, это вопросы отвалятся 😀
#ktor
Теперь принятие архитектурных решени и проектирование нового функционала для Ktor происходит публично в репозитории ktor-klip!
Первый KLIP на очереди – официальное решение для DI. Все желающие могут посмотреть какой планируется дизайн и повлиять на него на ранней стадии!
Если вы используете Ktor в качестве клиента, эти изменения пока никак вас не коснутся, они направлены на то чтобы упростить использование DI при написании серверов.
А здесь в комментариях можно похоливарить про "зачем ещё один DI?", "почему не Koin?", "будет compile-time валидация графа?". Хотя если прочитать документ, это вопросы отвалятся 😀
#ktor
Пробежавшись по постам за год я понял, что безнадёжно отстаю от трендов. Во-первых я ни разу не поругал дядюшку Боба, а во-вторых ни разу не бомбил про Gradle (предупреждение о сломанном релизе не в счёт). Негоже уходить в новый год с такими пробелами, поэтому буду исправляться. Хотя бы частично.
С дядюшкой Бобом и без меня неплохо справляются, я бы мог разве что на его стороне выступить, но это не модно. А про Gradle мне есть что сказать. Если у вас в этот момент возникла мысль "прочитаю лучше после праздников", подумайте, хотите ли вы начинать год с Gradle.
Существует много причин не любить Gradle, но меня больше всего раздражает его "хрупкость". Есть много способов сделать одно и то же, но только один из них правильный, а остальные приведут к замедлению конфигурации проекта, несовместимости с configuration cache или проект вообще перестанет собираться.
Речь, конечно, про lazy API. Это прям штука про которую нужно знать сразу, как только начинаешь делать в Gradle что-то сложнее чем объявление зависимостей. Но не стоит терять бдительность после прочтения документации. Допустим, ты знаешь, что нужно использовать
А если хотим сконфигурировать все таски определённого типа? Можно написать так:
И это будет ошибка. Если в
Хорошо, а если хотим выключать таски по какому-то условию? Например, по флажку в
Ой-ой, опять ошибка! Нужно использовать
Так что в новом году желаю вам выбирать всегда правильные APIшки (и не только в Gradle). С Рождеством и Новым Годом :)
#gradle
С дядюшкой Бобом и без меня неплохо справляются, я бы мог разве что на его стороне выступить, но это не модно. А про Gradle мне есть что сказать. Если у вас в этот момент возникла мысль "прочитаю лучше после праздников", подумайте, хотите ли вы начинать год с Gradle.
Существует много причин не любить Gradle, но меня больше всего раздражает его "хрупкость". Есть много способов сделать одно и то же, но только один из них правильный, а остальные приведут к замедлению конфигурации проекта, несовместимости с configuration cache или проект вообще перестанет собираться.
Речь, конечно, про lazy API. Это прям штука про которую нужно знать сразу, как только начинаешь делать в Gradle что-то сложнее чем объявление зависимостей. Но не стоит терять бдительность после прочтения документации. Допустим, ты знаешь, что нужно использовать
tasks.named("javadoc")
, а tasks.getByName("javadoc")
в большинстве случаев не нужно, так как этот вызов создаёт запрошенный таск на месте вместо того чтобы возвращать ленивый провайдер. Но что если нужно сразу сконфигурировать этот таск? Вроде всё просто:tasks.named("javadoc") { enabled = false }
А если хотим сконфигурировать все таски определённого типа? Можно написать так:
tasks.withType<Javadoc> { enabled = false }
И это будет ошибка. Если в
withType
передать лямбду, то под капотом вызывается withType<T>().all(configure)
, а all
в моменте создаёт все таски в коллекции. Правильно будет делать так:tasks.withType<Javadoc>()
.configureEach { enabled = false }
Хорошо, а если хотим выключать таски по какому-то условию? Например, по флажку в
gradle.properties
:tasks.withType<Javadoc>().configureEach {
enabled = properties["tasks.javadoc"].toBoolean()
}
Ой-ой, опять ошибка! Нужно использовать
findProperty("...")
. Почему? Посмотрите документацию к getProperties... а, погодите, там ничего полезного не написано... тогда документацию к Project (скриншот снизу). Этот метод ищет "свойства" в более широком смысле — смотрит на поля convention'ов, Gradle-экстеншены, поля внутри Project
, все таски, extras, причём не только для текущего проекта, но и для всех родительских. И все это собирается в одну большую Map'у. Привести это может к довольно неожиданным проблемам.Так что в новом году желаю вам выбирать всегда правильные APIшки (и не только в Gradle). С Рождеством и Новым Годом :)
#gradle