Ra'Reilly - Заметки про Ktor и не только
972 subscribers
71 photos
134 links
Раз в никогда тут появляются заметки.
В основном про около-Ktor, но иногда и про тулинг залетает.

Автор: @osipxd
Download Telegram
Чем больше узнаю про всяческие оптимизации которые делает компилятор и инструменты типа R8, тем меньше верю в код который пишу.

Был в нашем проекте такой код с рефлексией:
SnackbarHostState::class.java
.declaredFields
.first { it.type == Mutex::class.java }
Я был уверен, что этот код достаточно безопасный и устойчивый к обфускации, т.к. мы не завязываемся на имя поля, а находим его по типу. Поле с типом Mutex в SnackbarHostState единственное, что может пойти не так?
Но всё сломалось после очередного обновления зависимостей и Android Gradle Plugin. Релизные сборки начали падать с ошибкой NoSuchElementException: Array contains no element matching the predicate в месте вызова first.

Первым делом я проверил, что реализация класса не поменялась и поле mutex не пропало — оно на месте, да и если бы оно пропало, падали бы не только релизные сборки.
Тогда заглянул в реализацию функции Mutex() чтобы удостовериться, что возвращаемый тип по прежнему Mutex — тоже всё в порядке.
И вот тут закралось подозрение, что R8 просто заинлайнил вызов функции и напрямую подставил вызов конструктора, сломав тип поля при этом.

Просмотр dex файла показал, что так и есть — у поля mutex теперь тип MutexImpl (который, кстати, internal). Не, ну а почему бы и нет? Сайд-эффектов у Mutex() нет, поле mutex приватное, от изменения типа публичное API не пострадает, так что почему бы не заинлайнить?

Полечил проблему поменяв код внутри first так, чтобы находить любой тип, реализующий интерфейс Mutex:
Mutex::class.java.isAssignableFrom(it.type)

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

#r8
👍9
Если загуглить "как переподписать APK", скорее всего наткнётесь на советы типа "удалить из APK папку META-INF, прогнать zipalign и подписать APK заново".
Но если так сделать с Signature Scheme v2, будут проблемки.

При подписи через jarsigner (v1) все изменения действительно вносятся только внутрь META-INF. Достаточно удалить эту папку, чтобы получить свеженький, нетронутый подписями APK. Но в v2 к APK добавляется APK Signing Block, который так просто уже не удалишь.

На самом деле решение простое — вообще ничего не делать. apksigner теперь сам умеет переподписывать APK новой подписью.
А ещё можно воспользоваться моим скриптом (чтобы получить красоту как на скриншоте). Он уже учитывает всяческие тонкости, а заодно умеет подписывать AAB.

#snippet #signing
👍7
Сегодня в исходниках Retrofit наткнулся на доку к методу baseUrl. И вот казалось бы давно использую Retrofit, а прочитал её первый раз.
То есть я и до этого замечал закономерность, что если путь эндпоинта начинается со слеша, то он используется как абсолютный, а если слеша нет — как относительный, но доходил до этого через долгое тупление в код и просмотр запросов в сниффере. Ответ же оказывается всё это время был тут, рядом. Более того на эту доку есть ссылки из описания каждой аннотации-HTTP-метода, так что нельзя сказать, что документация расположена в неожиданном месте и её сложно найти.

Там же в доке есть описание как работает сериализация параметров, которые передаются с аннотациями @Path, @Query и т.д. По умолчанию просто вызывается .toString() и этого в большинстве случаев достаточно, но для сериализации каких-нибудь дат и времени будет полезно определить stringConverter в фабрике конвертеров.

Помню, что несколько раз заходил на "сайт" ретофита, но не находил там документации и шёл гуглить вместо того чтобы посмотреть исходники. При этом забавно, что я сам придерживаюсь точки зрения, что лучшее место для документации — как можно ближе к коду, в комментариях к классам и методам. Теперь сижу и думаю, точно ли это так? Я единственный так невнимательно отнёсся к доке Retrofit?
Давайте проверим. Если вы как и я пропустили эти моменты в доке Retrofit, ставьте 🤔
🤔10😁3
Вчера днём, когда я начинал писать про группировку ошибок в Crashlytics, я не представлял, что это выльется в такой длиннопост из костылей и велосипедов (5 минут на прочтение).
Надесь на обратную связь. 😉 Стоит ли продолжать тему и рассматривать альтернативный подход?
👍5🔥2
Всегда было интересно как реализованы динамические иконки календаря и часов. Иконка календаря показывает актуальную дату, а стрелки на иконке часов совпадают с реальным временем.
Ну то есть понятно, что это реализовано со стороны лаунчера, но интересно можно ли как-то сделать такое же для своего приложения... конечно же нельзя.
Судя по коду динамические иконки это просто хардкод для календаря и часов. При этом для этих иконок используются разные подходы:

🗓 Для календаря в loadCalendarDrawable лаунчер получает номер дня недели и загружает для него соответствующий drawable. Сами drawable берутся из Google-календаря.
🕦 Для часов же лаунчер отрисовывает иконку в ClockDrawableWrapper. При этом судя по коду для иконки даже предусмотрена возможность отображения секундной стрелки (отключена в коде) и есть анимация при смене положения стрелок.

Кстати, там же, в ThemedIconDrawable можно посмотреть как устроена отрисовка монохромных иконок с цветом темы.
👍9
Знаю, что среди подписчиков теперь есть дизайнеры. Не обещаю, что будет интересно, но иногда буду писать посты на стыке дизайна и разработки.

Сегодня хочу написать об ошибке вёрстки, которую встречаю на ревью чаще остальных👇
Когда я только начинал использовать 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 scripting. Плагин Kotlin, видимо, не учитывает "provided properties" и поэтому высвечивает ошибку в IDE.

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

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

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

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

#gradle #bug
🔥18