Ra'Reilly - Заметки про Ktor и не только – Telegram
Ra'Reilly - Заметки про Ktor и не только
975 subscribers
71 photos
133 links
Каждую неделю (нет) тут появляются заметки.
В основном про около-Ktor, но иногда и про тулинг залетает.

Автор: @osipxd
Download Telegram
Когда я только начинал использовать Material, я заметил, что кнопки в вёрстке располагаются неправильно. Вроде отступы выставляю как в дизайне, но они почему-то получаются больше. Оказалось у кнопки есть дополнительные отступы по 6dp снизу и сверху. Что я сделал вместо того чтобы вникать для чего эти отступы? Правильно — загуглил "How to remove padding around button". На SO никто тоже не задавался вопросом зачем эти отступы, поэтому я был уверен, что убрать отступы это единственное правильное решение. Но ведь вряд ли их добавили, только чтобы поломать вёрстку?

Возможно, вы уже догадались по прикреплённой картинке, во всём виноваты гайдлайны accessibility. Чтобы интерфейсом было удобно пользоваться всем, минимальная область нажатия (touch target) должна быть 48dp по ширине и по высоте. Стандартная material кнопка имеет высоту 36dp, поэтому чтобы добить её до 48dp снизу и сверху добавляются отступы по 6dp, которые входят в область нажатия.
В XML за эти отступы отвечают атрибуты insetTop и insetBottom, а в compose MinimumTouchTargetModifier.

В случае с обычными кнопками об аccessibility за нас позаботились разработчики Google. Всё гораздо хуже, когда появляются кнопки-иконки и текстовые кнопки.
Разработчик видит в дизайне иконку размером 24dp и просто добавляет иконку и вешает на неё возможность клика. С точки зрения вёрстки всё идеально, но вот нажимать кнопку с областью нажатия 24dp не слишком удобно. С текстовыми кнопками та же история. Если в дизайне текстовая кнопка выглядит как обычный текст, скорее всего разработчик так её и реализует — область нажатия такой кнопки по высоте будет и того меньше.

Разработчик, который помнит о минимальной области нажатия, обрекает себя на вечные упражнения по арифметике за 3-й класс. Добавление кнопки-иконки будет выглядеть примерно так: "Хм, у этой иконки размер 24, но надо добить его до 48 с помощью отступов. Это получается дополнительно по 12 с каждой стороны. А какие отступы у компонента в котором эта иконка? Ага, слева от края до иконки 16, а сверху 12. Значит вычитаем из этих значений 12, которые добавляем внутрь иконки и делаем отступ слева 4, а сверху 0."
И это ещё благоприятная ситуация, когда вокруг иконки хватает внешних отступов, чтобы компенсировать добавленные внутренние. А потом придёт другой разработчик, который не поймёт почему тут отступы отличаются от дизайна и приведёт их в соответствие дизайну. Вёрстка опять поехала :)

Что можно сделать:
👉 Для кнопок-иконок используйте IconButton, вместо Icon.
Если XML: сделайте отдельный стиль, в этом issue есть множество вариантов.
👉 Для текстовых кнопок используйте TextButton, вместо Text.
Если XML: кнопка со стилем Widget.MaterialComponents.Button.TextButton — наш выбор
👉 И самое главное — просите дизайнеров учитывать области нажатия компонентов в макетах. В идеале включить дополнительные отступы в кнопки и создать компоненты (символы) для текстовых кнопок и кнопок-иконок. Расскажите слёзную историю как неудобно для каждого элемента считать отступы в уме. Надеюсь удастся их убедить.

Пусть наши интерфейсы будут не только красивыми, но и удобными!

#ui #pains
👍74
Сегодня поста не будет, извините
Берегите себя.
🙏18💩1
В последнее время на работе часто рисую диаграммы для документации.
Сегодня расскажу какие инструменты для этого использую.

Для контекста — у нас большая часть документации пишется в Markdown файлах прямо в репозитории проекта. Отсюда вытекает требование, что хочется иметь возможность быстро редактировать диаграммы. Желательно чтобы для редактирования не нужно было ставить дополнительный софт.
Ещё не всегда хочется заморачиваться с красивым расположением элементов на схеме, лучше чтобы оно как-то само расположилось.

На помощь приходит Mermaid. Он позволяет описывать диаграмму текстом, а отрисовку берёт на себя.
Например, чтобы сгенерировать диаграмму для картинки сверху, понадобилось 4 строчки:

flowchart LR
start([Начало]) --> question{Пришла \n повестка?}
question -- Да --> trash[Выкинуть] --> endnode
question -- Нет --> endnode([Конец])

Само по себе то что диаграмму можно "запрогать" вместо того чтобы рисовать руками, это уже круто. Ещё круче, что в большинстве случаев вам не понадобится сохранять результат в виде картинки, потому что рендеринг Mermaid поддерживается в GitHub и GitLab. Всё что нужно, чтобы внести изменения в такую диаграмму — изменить код, описывающий её. Кстати, официальный плагин Markdown для IDEA умеет рендерить Mermaid.

Mermaid на данный момент поддерживает 11 видов диаграмм, среди которых помимо очевидных sequence- и class-диаграмм есть ещё диаграммы графа гита. Если этого недостаточно, посмотрите в сторону PlantUML. В нём больше видов диаграмм и возможностей для кастомизации. Синтаксис там посложнее и нет поддержки в GitHub, но всё ещё есть поддержка в GitLab и в IDE.

Иногда есть желание и вовсе затащить диаграмму в описание функции или класса. В этом случае можно использовать ASCII диаграммы. Те самые, которые вы наверняка не раз встречали во всяческих RFC (кстати вот вам RFC про ASCII Art :))
Для простеньких ASCII диаграмм можно использовать asciiflow. Есть ещё Diagon, он может в ASCII и красивую табличку отрендерить, и иерархию каталогов, и EBNF, и ещё много чего.

#tooling
👍6
Прошёл почти год с прошлого поста момента когда я написал "временную" либу для шифрования DataStore и два года с создания feature request в трекере 🎉
Раз мы всё ещё не увидели официального решения для шифрования от Google, я решил привести либу в приличный вид.

Что нового
1️⃣ Интеграция с security-crypto. Благодаря этой интеграции стало сложнее использовать либу неправильно. Больше не нужно получать Aead через Tink, вместо этого достаточно передать EncryptedFile при создании DataStore.
2️⃣ Шифрование в потоке. Раньше это было не так, файл для (де)шифровки считывался в ByteArray целиком.
3️⃣ Больше документации. Теперь помимо README, примеры использования методов есть в комментариях к самим методам.

⚠️ Чтобы при переезде на новую версию не потерять данные, посмотрите гайд по миграции.

* На скриншоте пример использования новой либы. Если использовать security-crypto-ktx:1.1.0, уйдут билдеры и код станет ещё проще

#datastore #security
🔥134
Про DataStore

Google представляет DataStore как замену SharedPreferences, а ещё часто называет его не иначе как Proto DataStore, подчёркивая что данные хранятся в формате protobuf. Такое позиционирование сбивает с толку.

1️⃣ Это не "замена SharedPreferences"

По крайней мере не прямая замена.
API для работы с Preferences DataStore сильно отличается от префов и приходится "обмазываться" экстеншенами, чтобы использовать его было хоть сколько-нибудь удобно. После первой попытки использования, "зелёные галочки" у DataStore в табличке сравнения не кажутся такими уж важными и чаще появляется мысль "нормально же жили, зачем что-то выдумывать". Вообще создаётся впечатление, что Preference DataStore сделали только для того чтобы переманить разработчиков с SharedPreferences и вышло не очень.

Первое что нужно сделать — выкинуть Preferences DataStore и хранить данные не формате "ключ - значение", а в нормальном классе с типизацией. Это может выглядеть так:

data class AppPreferences(
val onboardingShown: Boolean,
val forceDarkTheme: Boolean,
)

Есть плюсы и помимо типизации. Можно хранить не только примитивы и списки, но и любые типы данных которые можно сериализовать.

2️⃣ Protobuf не обязателен

И даже будет минусом, если:
- до этого с ним не работали
- не хочется (или нельзя) тащить новую зависимость в проект

Несмотря на то, что Google везде упоминает protobuf, его использовать не обязательно. Формат сериализации данных полностью контролируется сериализатором, который вы передаёте при создании DataStore. У меня для вас даже есть готовая функция для создания сериализатора в формате Json на основе kotlinx.serialization.
☝️Если всё-таки хочется оставить protobuf, посмотрите в сторону поддержки protobuf в kotlinx.serialization, возможно окажется удобнее чем официальная либа гугла.

ℹ️ TL;DR

Чтобы решить для себя нужен вам DataStore или нет, предлагаю рассматривать его в отрыве от SharedPreferences и protobuf. Это просто файл с сериализатором и возможностью реактивной подписки.

#datastore
👍6🔥3
🤖🤝🧑

Последние два месяца я каждый день решаю задачи на LeetCode. Могу похвастаться, что с уровня "ничего не понял" прокачался до уровня, когда справляюсь с Easy и Medium задачами, а на Hard уходит от нескольких часов до 1-2 дней 😀
Уже прогресс! А ещё я подтянул Rust до уровня когда могу писать решения задач на нём.

Первый месяц я решал задачи в гордом одиночестве, а потом подумал, что классно было бы с кем-то обсуждать задачи и вместе находить решения.
У нас в rmr 😎 как раз есть алгоклуб. Немного поразмыслив с Владом, решили на его основе запустить алгоритмический марафон.

Второй месяц — мы решали задачи все вместе. Обсуждали, делились решениями, готовили доклады для более глубокого изучения часто встречающихся алгоритмов и подходов, разбирали наиболее сложные задачи на встречах клуба. Больше половины участников всё ещё держатся и решают задачи каждый день, так что эксперимент считаю успешным.

Третий месяц — будущее. Формат прошёл внутреннее и бета-тестирование, он готов к релизу! Мы хотим распространить алгоритмический марафон на всех желающих. Тем более что в декабре начинается очередной Advent of Code с его дружелюбными и фановыми задачами.
Предлагаю собраться на нашем Discord-сервере и вместе решать Advent of Code каждый день с 1 по 25 декабря 🎄
Заходи и зови друзей!

#анонс
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥10🍾2👍1
Есть такой замечательный сайт, на котором можно найти бесплатные векторные иконки популярных брендов, называется simpleicons.org. Помимо самой иконки, там указан фирменный цвет и иногда заботливо прикреплены ссылки на гайдлайны по использованию и лицензию. Все эти иконки можно использовать в бейджах shields.io.

Вы не найдёте разве что лого Java и всего что принадлежит Disney. Потому что Oracle не любит когда кто-то берёт их чашку, а с Disney никто даже связываться не хочет.
🔥7😁2
This media is not supported in your browser
VIEW IN TELEGRAM
Давайте немножко расскажу про Advent of Code :)

Сегодня была очень простая задача с точки зрения логики. Сложность в том, что чтобы хотя бы начать решать задачу, надо прочитать списки из входных данных. Списки же красиво выстроены столбиками, а не строчками.
Я потратил около 10 минут на то чтобы всё-таки написать парсинг, а потом понял, что вместо этого можно было перевернуть столбики в горизонтальное положение с помощью мультикурсора и истории копирований в IDEA за 30 секунд.

Вообще, пока что затеей с Advent of Code доволен. Получается весело и полезно :D Есть люди, которые решают задачи в Excel, я пощупал Scala, а один участник вообще написал для предыдущего дня решение в стихах на Rockstar 🎸

P.S. Пока готовлюсь к митапу (упс, спойлеры) нет времени писать полноценные посты, но это ненадолго

#idea #aoc
🔥6
🗓 В этот четверг, 15 декабря, в 19:00 по МСК у нас в роботохранилище пройдёт митап.

Влад расскажет про дизайн-ревью и покажет клёвые инструменты для этого. Рома — про то как раскапывал систему цветов в Material You и про content-based colors. В конце я буду вещать про инфраструктуру наших проектов и автоматизации.

Можно подключиться онлайн, а можно прийти в офис (он у нас классный). Все подробности и более кликбейтные анонсы смотри на TimePad. Регистрация там же.
До встречи в четверг 🤖

#анонс
🔥4👍3
🚨 Немного поздно, конечно, но если у вас в проекте есть зависимости с jitpack.io, включите offline mode в Gradle и не чистите кэши зависимостей.
Jitpack лежит второй день. При этом status.jitpack.io говорит, что всё хорошо 🤷

UPD: А если jitpack объявлен выше других репозиториев, он может и другим зависимостям мешать стянуться. В этом случае exclusive content в помощь и стоит опустить его пониже.

UPD2: В твиторе тоже пусто. Бедняги. Вот-вот рождество наступит, а им вместо последних приготовлений надо поднимать сервера 😢

UPD3: Всё починили. Но полезно знать что делать в подобной ситуации в будущем (спасибо @lionzxy за инструкцию)
👍6
🎁 Давно хотел рассказать про автоматизации и инфраструктурные штуки, которые используем на проектах и наконец рассказал. На очереди теперь записать часть про CI/CD, которая не влезла, но это уже в следующем году.

Такой вот подарочек. Всех с праздниками! Наступающими и наступившими (в зависимости от того когда вы это читаете)

Оффтоп:
Кстати, давным-давно, я писал, что у московских роботов-андроидов появился YouTube канал, призывал подписываться, ставить лайки и т.д. Так вот можно от него отписываться :D
Теперь нет отдельного канала MSK и SPB роботов — всё в одном канале red_mad_dev. Планируем новый контент выпускать только туда.

#анонс #tooling
🔥8👍41
Внезапный анонс! Сегодня в 15:00 по МСК у нас будет проходить открытая встреча алгоклуба, где Андрей Толмачёв расскажет про структуру данных Binary Heap. Заглядывайте в дискорд :)

Да, в последнее время тут ничего кроме анонсов нет, но обещаю что это последний анонс пред другим контентом :)

#анонс
👍3
Если вы использовали Gradle Version Catalogs, наверняка видели такую ошибку в каждом build-скрипте. Чтобы от неё избавиться даже плагин написали, ну а простые работяги добавляли @Suppress("DSL_SCOPE_VIOLATION") в каждый build-скрипт.

Так вот, свершилось! Эту проблему пофиксили и фикс войдёт в Gradle 8.1 🎉

Разберёмся в чём был баг👇
2
Ребята из Gradle не сразу поняли в чём проблема - код светится красным, но при этом всё собирается и работает. Логично предположить, что баг на стороне IntelliJ. Ребята из JetBrains исследовали проблему и выявили что она всё-таки на стороне Gradle, т.к. действительно есть нарушение DSL скоупа, Kotlin выдаёт верную ошибку и каталоги вообще не должны были работать внутри блока plugins.

Почему не должны были?
Дело в том, что блок plugins особенный. Попробуйте на верхнем уровне build-скрипта объявить переменную, а потом использовать её внутри plugins, Kotlin её просто не увидит. Если посмотреть на описание класса PluginDependenciesSpecScope, там явно написано:
This class exists for the sole purpose of marking the plugins block as a GradleDsl thus hiding all members provided by the outer KotlinBuildScript scope.
То есть не должно быть возможности внутри блока plugins использовать ничего извне.
Аксессоры для каталогов генерируются как экстеншены на Project, то есть и они не должны были быть доступны внутри блока plugins. Но всё-таки они работали т.к. добавлялись через provided properties, механизм в Kotlin noscripting. Плагин Kotlin, видимо, не учитывает "provided properties" и поэтому высвечивает ошибку в IDE.

В Gradle 8.1 будут генерироваться легальные аксессоры для доступа к каталогам внутри блоков plugins и buildnoscript. Помимо того что это позволяет исправить нарушение DSL скоупа, это позволяет ещё и не генерировать аксессоры для бандлов и библиотек внутри plugins, т.к. их там всё равно нельзя использовать. Правда для обратной совместимости они всё равно генерируются, но с аннотацией @Deprecated.

Очередная история про то как важно понимать причину проблемы. Баг зарепортили полтора года назад — в августе 2021, а пофиксили в течении трёх месяцев после того как стало понятно где именно проблема.

Мораль: если вы репортите issue и у вас есть возможность самостоятельно провести исследование проблемы, шансы на быстрый фикс возрастают. Ваш кэп.

UPD: Что-то пошло не так и фикс будет в 8.2 Пронесло

#gradle #bug
🔥18
Обычно мы на всех проектах добавляем тип сборки QA. Это такая сборка, которая максимально близка к релизной, но может включать в себя дебаг-панель и позволяет подключиться к приложению через дебаггер.
И вот недавно оказалось, что схема не работает. QA сборки не обфусцируются, а значит не сильно отличаются от дебажных. Причём не понятно в какой момент отвалилась обфускация и из-за чего это произошло.
Расследование привело к багтреккеру гугла: "AGP 7.2.0 onwards does not obfuscate our built app" и issue закрыто как Won't fix (Intended behavior) 🙂

Дело в том, что флаг isDebuggable = true теперь отключает обфускацию и по задумке инженеров гугла так должно было работать всегда. Ведь вы не сможете бегать по коду дебаггером, если он отличается от исходников, какой же это тогда получается debuggable? То что раньше была возможна обфускация с isDebuggable = true это изначально ошибочное поведение и AGP никогда не должен был так работать. Видимо ошибка показалась настолько очевидной, что фикс даже не упомянули в release notes, а как известно every change breaks someones workflow, что и случилось.

Решение подсказали там же в issue. Новый API, добавленный в AGP 7.0 4.2 позволяет вклиниться в процесс подготовки артефактов сборки. Можно подменять, добавлять и изменять промежуточные файлы, например: манифест, APK и даже использовать совсем чёрную магию — модифицировать уже скомпилированные классы. Хотелось когда-нибудь модифицировать один класс в библиотеке, чтобы не ждать исправления бага?
Но сейчас не об этом. Нам интересен манифест. С помощью этого API мы можем дописать любой текст в смердженный манифест, в частности и флаг debuggable.

👨‍🦽 Результат можно увидеть в фиксе для gradle-infrastructure или в отдельном сниппете.

P.S. Помимо флага в манифесте приходится ещё руками менять значение BuildConfig.DEBUG ведь он тоже зависит от isDebuggable.

#agp #snippet
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12😁2🔥1
Насмотренность — важный навык в работе дизайнера. Это то что помогает быть в тренде, учиться на чужом опыте, черпать вдохновение, отличать хороший дизайн от плохого, критически оценивать свою работу и так далее. Нет вы не ошиблись каналом, просто я считаю, что насмотренность важна и разработчикам. Чем больше чужого кода смотришь — тем лучше пишешь свой. Просто потому что больше новых приёмов и концепций узнаёшь, улучшаешь свой скилл владения языком. С хэштегом #насмотренность буду писать про всё интересное на что наткнулся при просмотре open-source проектов. Это первый пост :)

👀 Companion object
Чаще всего companion object используется просто как замена статическим методам и полям из Java. А ведь companion object это в первую очередь object и как у любого другого объекта у него может быть имя и он может реализовывать интерфейсы и наследоваться от других классов. Сегодня будет два примера таких companion object'ов.

☝️ Json в kotlinx.serialization
Это класс, но мы можем использовать его как будто он object:
val result = Json.decodeFromString("42")


Всё потому что companion object у Json реализует класс Json:
companion object Default : Json(JsonConfiguration(), EmptySerializersModule())


Зачем так сложно, если можно было бы просто объявить object Json? Потому что тогда не было бы возможности создать свой инстанс, а когда Json это класс, такая возможность остаётся.

✌️CoroutineContext.Key в kotlinx.coroutines
CoroutineContext вообще интересная штука. По сути это что-то между Map и Set. Каждый элемент контекста хранится в CoroutineContext по ключу и получить элементы контекста можно через оператор get.
Каждый ключ должен быть singleton'ом и ключ должен быть у каждого элемента контекста (Job, CoroutineDispatcher и т.д.). Идеальная задача для companion object!

У каждого элемента контекста companion object это реализация CoroutineContext.Key и смотрите какая красота получается в совокупности с оператором get у CoroutineContext:
val job = coroutineContext[Job]
val dispatcher = coroutineContext[CoroutineDispatcher]


Выглядит как-будто получаем элемент контекста по типу. На самом деле используется companion object c типом Key, объявленный в этих классах. Например, в Job он выглядит так:
companion object Key : CoroutineContext.Key<Job>
👍9🔥4
Я в прошлом году писал про минимальный размер элементов вёрстки, чтобы на них было легко нажимать. В compose для соблюдения этого правила даже был Modifier.minimumTouchTargetSize, который использовался в Material компонентах, но почему-то был internal.
Так вот в compose 1.4.0 его переименовали в Modifier.minimumInteractiveComponentSize и сделали публичным 🎉
👍103
Серёжа Чумиков сделал классный доклад про оптимизацию количества рекомпозиций в Compose. Если формат видео вам заходит больше чем статьи — вот оно!

По секрету скажу, что есть ещё одна запись доклада — про оптимизации анимаций. Надеюсь её тоже выложат.

#compose
🔥11👍2
За последние две недели вышло много крупных обновлений — Kotlin 1.8.20, AGP 8.0.0, Gradle 8.1 и новая Android Studio. Я пообещал себе не превращать канал в поток сообщений об обновлениях, но иногда изменения цепляют сердечко и я не могу устоять перед соблазном написать про них.

В этот раз порадовал Gradle.
👉 Во-первых, поддержку version catalogs в блоке plugins {}, про которую я недавно писал, всё-таки включили в релиз 🎉
👉 Во-вторых, в Kotlin DSL завезли поддержку оператора = для lazy properties. То есть теперь можно писать так:

redmadrobot {
android {
// Вместо такого
minSdk.set(28)
// Вот так
minSdk = 28
}
}


Чтобы это всё заработало понадобится:
• Kotlin 1.8.20 (на самом деле достаточно 1.8.0, но в последней версии пофиксили важные баги)
• IntelliJ IDEA 2022.3+ (или Android Studio Giraffe)
• добавить в gradle.properties строчку systemProp.org.gradle.unsafe.kotlin.assignment=true

🪄 Как это работает в Kotlin?

В Groovy DSL, к слову, так можно было делать всегда. Но на то он и Groovy, чтобы делать всякую магию, нам же интересно как это работает в Kotlin.

После компиляции строка minSdk = 28 превращается в minSdk.assign(28), а реализация assign выглядит так:

fun <T> Property<T>.assign(value: T?) {
emitIncubatingLogMessage()
this.set(value)
}


За это превращение отвечает плагин компилятора assignment-plugin. Он позволяет пометить класс специальной аннотацией и у такого класса можно будет "переопределить" оператор присваивания.

Официальной документации на плагин нет, но если захотите применить у себя в проекте, я написал небольшую доку с примером применения, требованиями и ограничениями, которые удалось собрать из исходников. Например, для меня стало сюрпризом, что переопределённый оператор присваивания нельзя использовать с локальными переменными или параметрами функции. Из-за этого ограничения долго не мог понять почему плагин у меня не работает.

Возможно это не так, но есть ощущение, что плагин написан специально для Gradle. К тому же, Kotlin Gradle Plugin недавно переехал на lazy properties и такая фича будет как нельзя кстати, чтобы упростить миграцию для пользователей.

Использовать или нет?

Да. Может пугать то, что фича в экспериментальном статусе, но если вы используете свежий Gradle, Kotlin и IDE, не вижу причин не использовать.
Сам Gradle уже применяет assignment operator в своём проекте.
А если вы автор плагинов, у вас не осталось причин не использовать lazy properties, тем более что теперь пользователи даже не заметят этого и не увеличится разница между Kotlin и Groovy DSL.

Вообще, рекомендую прочитать список изменений Gradle 8.1, там ещё configuration cache стабилизировали, сделали более полезными сообщения об ошибках компиляции Kotlin DSL и т.д.
#gradle #kotlin
👍10
Оказалось, что фича с адаптивными emoji, которые я исопльзовал в предыдущем посте, поддерживается не во всех клиентах. Я конечно завёл баги (macOS, Web K, Web Z), но хочется понять сколько людей затронула проблема (статистики по клиентам у меня нет).
Final Results
33%
Работает - все emoji-логотипы под цвет текста
67%
Не работает (клиент для macOS)
0%
Не работает (Web)