Ra'Reilly - Заметки про Android и не только
رفتن به کانال در Telegram
Каждую неделю (нет) тут появляются заметки. Пока я не сдуюсь. В основном про Андроид, но иногда и про тулинг залетает. Автор: @osipxd
نمایش بیشترروسيا844 407دسته بندی مشخص نشده است
262
مشترکین
اطلاعاتی وجود ندارد24 ساعت
اطلاعاتی وجود ندارد7 روز
اطلاعاتی وجود ندارد30 روز
آرشیو پست ها
Не верю своим глазам! В Телеге добавили подсветку синтаксиса!
fun finallyCodeIsHighlihted() {
println("Hello, highlighting!")
}
Ещё пару дней назад подсветку видел только в Web-овском и десктопном приложении, а сегодня и в мобильном заметил.Итак полный набор условий воспроизведения:
👉 API 24-27 (28 всегда использует
ViewLayer, это его спасло)
👉 для вьюхи включено аппаратное ускорение
👉 применён Modifier.rotate
👉 применён Modifier.clip с формой без поддержки обрезки силами платформы - на эту роль подходит любой Shape, возвращающий Outline.Generic, среди стандартных это CutCornerShape
Соединяем всё вместе и получаем красивую гифку! Слева баг, справа ожидаемое поведение. Осталось только пофиксить.Во время экспериментов с разными реализациями
Shape, заметил, что баг проявляется только при использовании clip, а если передавать параметр shape в background, всё отрисовывается правильно.
По коду интересно понять две вещи:
👉 Почему с одними формами баг воспроизводится, а с другими нет?
👉 Почему clip сломан, а background работает правильно?
Начнём с реализации background т.к. она проще. Модификатор background создаёт BackgroundNode. В этой ноде логика простая — рисуем фон, с переданной формой и цветом, а поверх него рисуем контент 1️⃣. Тут видно, что для определения формы используется не непосредственно Shape, а Outline, который мы из него получаем. Стоит остановиться на этом подробнее.
Shape определяет правила создания формы и может содержать дополнительные настройки, определяющие форму. Например, мы можем определить размер углов у всех наследников CornerBasedShape. Outline же это созданная для конкретного размера, ориентации layout’а и плотности пикселей форма, готовая к отрисовке. Так как Shape только определяет логику создания Outline, его можно безопасно переиспользовать для разных composable-функций. Лучше всего добавить используемые повсеместно формы в тему - так и переиспользовать их получится, и лишних аллокаций внутри composable-функций не будет.
Если Shape это интерфейс, который можно реализовывать как угодно, то Outline это sealed class с тремя возможными реализациями: Rectangle, Rounded и Generic 2️⃣. Первый кусочек пазла есть — RoundedCornerShape использует Outline отличный от GenericShape, через который мы реализовали нашу форму. Осталось только понять в чём разница между этими outline’ами.
Посмотрим как работает clip внутри. Clip это частный случай использования graphicsLayer. Если пропустить все промежуточные сущности и посмотреть где используется переданный shape, мы увидим, что он передаётся в качестве параметра в OwnedLayer.updateLayerProperties 3️⃣. У OwnedLayer есть две реализации - RenderNodeLayer и ViewLayer. Из кода ComposeView видно, что RenderNodeLayer создаётся если включено аппаратное ускорение, а ViewLayer - если оно недоступно 4️⃣.
Мы можем вручную отключить программное ускорение, чтобы проверить воспроизводится ли баг на ViewLayer. Для этого добавим в манифест флаг hardwareAccelerated = false и… баг пропал. То есть проблему можно “исправить” отключив аппаратное ускорение для вьюхи. В случае с компоузом получается, что отключать ускорение придётся для всего экрана, так как это всё одна вьюха. Так делать не хочется, но запомним этот вариант.
Из реализации RenderNodeLayer.updateLayerProperties становится понятно, что shape передаётся в OutlineResolver 5️⃣, который в свою очередь выбирает из двух вариантов отрисовки формы 6️⃣:
1. Если платформа поддерживает обрезку для такого типа Outline’а, ничего не делаем - о форме вьюхи позаботится RenderNode.
2. Если для Outline не поддерживается обрезка, выставляется флаг usePathForClip = true и вызываем clipPath на Canvas 7️⃣.
Для каких outline’ов поддерживается обрезка, а для каких нет? Это определяет метод Outline.canClip() 6️⃣. Тут важно не пропустить комментарий к методу - начиная с API 33, метод всегда возвращает true, но в более ранних версиях Android true возвращается только для прямоугольника, круга и закруглённого прямоугольника.
Теперь понятна разница между разными формами. Наша кастомная форма и RoundCornerShape обрезаются через разные механизмы, и вариант обрезки через clipPath даёт сбой. Почему? Не знаю, может инженеры Гугла подскажут мне в issue.Итак полный набор условий воспроизведения:
👉 API 24-27 (28 всегда использует
ViewLayer, это его спасло)
👉 для вьюхи включено аппаратное ускорение
👉 применён Modifier.rotate
👉 применён Modifier.clip с формой без поддержки обрезки силами платформы - на эту роль подходит любой Shape, возвращающий Outline.Generic, среди стандартных это CutCornerShape
Соединяем всё вместе и получаем красивую гифку! Слева баг, справа ожидаемое поведение. Осталось только пофиксить.🧭 Раскопать код Compose
Читать в телеге разбор кода с иллюстрациями такое себе удовольствие, это нужно постоянно прыгать от картинок к тексту и наоборот. Чтобы хоть чуть-чуть облегчить эту задачу:
1. Добавил цифры от 1️⃣ до 7️⃣ на картинки и в текст, чтобы было понятно какую иллюстрацию смотреть и можно было проще найти место в тексте на котором остановился. Цифры поставлены в то место в тексте, из которого по моему мнению логично прыгнуть в картинку, а потом вернуться.
2. Постарался в тексте кратко описать всё что происходит на картинках, чтобы и без них было всё понятно. Но тут вам судить насколько это получилось.
Но чтобы читать было максимально комфортно, придётся два устройства использовать. На одном картинки, на другом текст 😅
👇👇👇
Во время экспериментов с разными реализациями
Shape, заметил, что баг проявляется только при использовании clip, а если передавать параметр shape в background, всё отрисовывается правильно.
По коду интересно понять две вещи:
👉 Почему с одними формами баг воспроизводится, а с другими нет?
👉 Почему clip сломан, а background работает правильно?
Начнём с реализации background т.к. она проще. Модификатор background создаёт BackgroundNode. В этой ноде логика простая — рисуем фон, с переданной формой и цветом, а поверх него рисуем контент 1️⃣. Тут видно, что для определения формы используется не непосредственно Shape, а Outline, который мы из него получаем. Стоит остановиться на этом подробнее.
Shape определяет правила создания формы и может содержать дополнительные настройки, определяющие форму. Например, мы можем определить размер углов у всех наследников CornerBasedShape. Outline же это созданная для конкретного размера, ориентации layout’а и плотности пикселей форма, готовая к отрисовке. Так как Shape только определяет логику создания Outline, его можно безопасно переиспользовать для разных composable-функций. Лучше всего добавить используемые повсеместно формы в тему - так и переиспользовать их получится, и лишних аллокаций внутри composable-функций не будет.
Если Shape это интерфейс, который можно реализовывать как угодно, то Outline это sealed class с тремя возможными реализациями: Rectangle, Rounded и Generic 2️⃣. Первый кусочек пазла есть — RoundedCornerShape использует Outline отличный от GenericShape, через который мы реализовали нашу форму. Осталось только понять в чём разница между этими outline’ами.
Посмотрим как работает clip внутри. Clip это частный случай использования graphicsLayer. Если пропустить все промежуточные сущности и посмотреть где используется переданный shape, мы увидим, что он передаётся в качестве параметра в OwnedLayer.updateLayerProperties 3️⃣. У OwnedLayer есть две реализации - RenderNodeLayer и ViewLayer. Из кода ComposeView видно, что RenderNodeLayer создаётся если включено аппаратное ускорение, а ViewLayer - если оно недоступно 4️⃣.
Мы можем вручную отключить программное ускорение, чтобы проверить воспроизводится ли баг на ViewLayer. Для этого добавим в манифест флаг hardwareAccelerated = false и… баг пропал. То есть проблему можно “исправить” отключив аппаратное ускорение для вьюхи. В случае с компоузом получается, что отключать ускорение придётся для всего экрана, так как это всё одна вьюха. Так делать не хочется, но запомним этот вариант.
Из реализации RenderNodeLayer.updateLayerProperties становится понятно, что shape передаётся в OutlineResolver 5️⃣, который в свою очередь выбирает из двух вариантов отрисовки формы 6️⃣:
1. Если платформа поддерживает обрезку для такого типа Outline’а, ничего не делаем - о форме вьюхи позаботится RenderNode.
2. Если для Outline не поддерживается обрезка, выставляется флаг usePathForClip = true и вызываем clipPath на Canvas 7️⃣.
Для каких outline’ов поддерживается обрезка, а для каких нет? Это определяет метод Outline.canClip() 6️⃣. Тут важно не пропустить комментарий к методу - начиная с API 33, метод всегда возвращает true, но в более ранних версиях Android true возвращается только для прямоугольника, круга и закруглённого прямоугольника.
Теперь понятна разница между разными формами. Наша кастомная форма и RoundCornerShape обрезаются через разные механизмы, и вариант обрезки через clipPath даёт сбой. Почему? Не знаю, может инженеры Гугла подскажут мне в issue.🧪 Условия воспроизведения
Прежде чем фиксить нужно понять, а есть ли проблема. У меня баг не воспроизводился, поэтому запросил информацию об окружении — какое устройство, какая версия Android. Устройство оказалось ни при чём, потому что это был эмулятор, а вот на API 27, на котором обнаружили баг, он воспроизвёлся.
Но насколько всё серьёзно? Проблема только на API 27 или на других версиях тоже? Чтобы определить границы затронутых версий можно использовать бинарный поиск. Знаем, что на API 27 воспроизводится, а на 33 - нет. Представляем 27..33 в виде массива чисел. Берём версию посередине - 30, проверяем на ней. Не воспроизвелось — ищем в левой половине, воспроизвелось — в правой и повторяем пока не найдем последнюю версию в которой баг воспроизводится. Искать стоит не только вправо от API 27 (в более новых версиях), но и влево, т.к. есть шанс, что на более старых версиях баг не воспроизводился.
Применив бинарный поиск в обе стороны, я выяснил, что баг затрагивает API 24-27.
💡 Возможно это известный баг?
Раз баг есть только на некоторых версиях Android, а у нас не самая последняя версия Compose, может его уже нашли и исправили?
Это было бы самым простым решением, но к сожалению ни поиск по Issue tracker’у, ни обновление на последний Compose ничего не дали. Значит придётся самим разбираться.
⚙️ Минимальный пример воспроизведения
Чтобы точнее локализовать проблему, нужно избавиться ото всего лишнего, что не влияет на воспроизведение бага.
В моём случае одна Composable-функция была вложена в другую функцию, это всё было внутри дизайн системы в другом репозитории, а на уровне проекта обёрнуто ещё в несколько Composable-функций. Причём
clip применялся ко внутренней вьюхе, а rotate к одной из обёрток. Слишком много потенциальных мест где может быть ошибка.
Cкопировал внутреннюю Composable-функцию, к которой применяется clip, и добавил к ней rotate. Убрал лишние модификаторы. После каждого изменения проверял сохраняется ли баг. В итоге получил тот самый Box с несколькими модификаторами, код которого был в вопросе.
Найден минимальный набор модификаторов, приводящих к багу, осталось подобрать минимальный набор параметров для них. C background ничего особенно не придумать, а вот внутри clip у нас использовалась сложная самописная форма, которая имитирует squircle “чтоб было как в iOS“. Раз ничего нагуглить по багу не удалось, может проблема воспроизводится только у нас из-за нашей формы?
Попробовал подставить RoundedCornerShape - баг перестал воспроизводиться! Похоже и правда что-то не то с реализацией squircle, попробую максимально упростить форму, чтобы проверить догадку. Вместо сложной формы нарисовал просто треугольник и… баг воспроизвёлся. Выходит форма ни при чём, но остаётся вопрос почему тогда с RoundedCornerShape всё в порядке? Пришло время залезть в исходники Compose.🔎 Воспроизвести и понять причину
Да-да, я украл стиль оформления постов у @android_easy_notes. Кстати классный канал, советую
👇👇👇
Что ж. Прошло достаточно времени чтобы все проголосовали (и забыли про это), можно и правильный ответ сказать.
Если исходить из доки и из того как это должно работать, правильным будет 3-й вариант ответа потому что
clip будет обрезать не только фон самого Box’а, но и контент внутри него, а shape переданный в Modifier.background влияет только на фон. На практике же всё немного сложнее.
Недавно сверстал вьюху в которой как раз использовал rotate и clip вместе. Сверстал, проверил, что всё работает и забыл, а через пару недель мне прилетает баг, что она выглядит мягко говоря странно (см. скриншот. Я не уверен, что могу публиковать скриншоты из реального приложения, поэтому вместо красивой вёрстки тут будет некрасивый красный квадрат с обрезанными углами. Но сути это не меняет и для объяснения бага достаточно).
С такими странными багами всегда не понятно в какую сторону копать. Интересно было бы написать пост про решение подобных проблем, подумал я, и даже придумал перед постом сделать голосовалку. Для интриги. А потом бац! И через неделю (наивный) правильный ответ с объяснениями написать.
А потом я набросал план поста и понял, что получается как-то обширно, и совершенно не понятно как это оформить в пост. Ведь хочется добавить картинок, кусков кода, и вообще как-будто больше похоже на статью… но если я уйду пилить статью, это ещё на несколько месяцев затянется. К тому же для Хабра как-будто недостаточно хардкорно. Может на Медиуме страничку завести? Или поднять себе блог на Jekyll или Hugo? Хм, какой движок для блога лучше? А стоит ли оно того? Интересна ли вообще кому-то эта тема? (ну вы поняли чем я занимался этот месяц). Чем дольше мусолю тему в голове, тем менее интересной она мне кажется и меньше хочется что-то про неё писать, так что решил разбить “пост” на логические куски и выкладывать их сюда по готовности.
Да простят меня все, кто любит смотреть сериал целиком.
План такой:
👉 Воспроизвести и понять причину
👉 Раскопать код Compose
👉 Найти workaround или фикс
#bug #composeБудет ли отличаться результат (UI) в левом (clip + background) и правом (background) варианте кода?
Перед очередным постом внезапный интерактив.
Сверху можно посмотреть код на Compose, который рисует красный квадрат со скошенными углами, повёрнутый на 45 градусов. Вокруг квадрата ещё рисуется синяя линяя (у неё углы не скошены).
Снизу вопрос👇 Чтобы на него ответить можно использовать IDE с превью, исходники Compose, эмулятор и вообще что угодно.
Как-то пропустил, что в IDEA появилась поддержка GitLab, аналогичная той что была для GitHub. Можно смотреть список MRов, ревьюить код и вливать их прямо из IDE, но есть нюанс.
Работает это пока что только для GitLab EE 15.10+. Поддержку более старых версий GitLab, а заодно Community Edition, обещают добавить в следующем bugfix релизе (issue: IDEA-326663).
JetBrains и GitLab разрабатывают плагин совместно. Поставить плагин можно начиная с IDEA 2023.2, так что даже канареечная студия пока что в пролёте.
#idea #git
Тут давно ничего не было, но для этого есть причина. Последние 3 месяца я готовился к большому автомобильному путешествию от Москвы до Черногории, а сейчас, собственно, в этом путешествии нахожусь.
На "запиливание контента" сил пока нет, но я решил прервать молчание не чтобы пожаловаться, а чтобы сделать, как я это умею, супер-запоздалый анонс :)
Сегодня в 19:00 по МСК буду участвовать в стриме на Android Guards, приходите посмотреть. Я пока не знаю что надо будет делать, но тем интереснее.
UPD: Появилась запись. Было интересно посмотреть со стороны хакеров как можно эксплуатировать разные уязвимости, в том числе те которые на первый взгляд кажутся незначительными.
Repost from _rnd
Какую анимацию выбрать: Composable или Suspend? Возможна ли анимация за ноль рекомпозиций? А что будет, если «обмануть» Compose и поставить @ Immutable на мутабельное значение? Об этом и многом другом рассказал Android-разработчик red_mad_robot Серёжа Чумиков в своём докладе.
Запись выступления можно посмотреть на нашем ютуб-канале, а презентацию скачать с гугл-драйва. Если пропустил первую часть доклада, читай этот пост.
#android #compose
А вы знали, что в IDEA (ну и в Android Studio) можно поменять иконку проекта?
Я не знал, а это возможно. Надо всего лишь... Нет, сода ни при чём. Надо на welcome-скрине нажать "Change Project Icon..." и выбрать иконку. Другой вариант — положить файл с названием
icon.svg или icon.png в папку .idea и ваш проект будет отличаться в списке проектов от всех остальных.
Интересно, что возможность такая есть уже давно. Я пытался найти когда она появилась, но нашёл только косвенные признаки, что в IDEA 2021 она уже была.
Всё, мне некогда, я пошёл ставить иконки всем проектам.
#idea🤨 Какие проблемы есть у Docker-образов с Android SDK?
Они тяжёлые.
Чаще всего публичные образа собираются по принципу "это тоже может понадобиться, и вот это надо добавить". Даже "минимальный образ" обычно содержит около 5 версий Android, ещё пяток версий build-tools, NDK, cmake, эмулятор и вот уже как минимум 5 гигов занято. А чтобы образ подошёл всем, можно докинуть ещё Node.js, Flutter SDK и Ruby с Fastlane 👍
Вес образов не проблема до тех пор пока ты их просто используешь as-is. Но в один прекрасный момент появляется потребность что-то добавить в образ, чтобы не ставить это "что-то" перед каждой сборкой и сэкономить на этом время.
Думаешь "возьму текущий образ как базовый и на его основе наклепаю свой", пишешь Dockerfile, пробуешь собрать локально и... Бац! Место кончилось — ну ничего, почистим диск. Бац! На M1 образ не собирается. И вот ты уже читаешь как собрать образ под определённую платформу и всё глубже погружаешься в недра докера.
Позже хочется ещё что-то поменять и становится понятно, что если хочешь образ в котором будет только то, что тебе нужно, собери его сам. Этот путь не легче и не в последнюю очередь потому что не понятно что нужно, а что нет, это следующая проблема.
Google не помогает.
Я не про поисковик, а про команду Android.
Эталонный официальный пример Docker-образа закрыл бы многие вопросы. Но такого образа нет и чем глубже ты погружаешься в тему создания своего образа, тем больше странностей и вопросов встречаешь:
👉 Зачем нужен каждый пакет в sdkmanager? Где взять их описание?
👉 Кто-нибудь знает для чего в Docker-образа ставят пакеты
extra;m2repository? Для чего скачивать локальный maven репозиторий, если мы его никак не подключаем в Gradle? Может AGP его подключает? Я тоже так подумал, но не нашёл этому никаких подтверждений. Убрал установку этих пакетов — ничего не сломалось 🤷
👉 Почему build-tools тянет за собой emulator? А, по ошибке видимо. Начиная с 31.0.0 уже не тянет.
👉 Блин. Сборка падает потому что llvm-rs-cc не может найти libncurses5. А, эту проблему тоже поправили начиная с build-tools 31.0.0
👉 Надо ли ставить несколько версий build-tools или можно поставить только самую новую? Есть ответ на SO, что версии обратно совместимы, но не звучит как авторитетный источник, да? Changelog крайне скупой, а реально полезную информацию можно найти разве что в исходниках. Для себя решил, что лучше использовать последнюю версию по причине из прошлых пунктов.
👉 Надо ли ставить platform-tools? Кажется что для сборки они не нужны, но AGP использует adb для всякого, так что подразумевает, что platform-tools установлены.
👉 Какие переменные окружения надо выставлять? Так... Надо выставить ANDROID_HOME... а погодите, она deprecated. Вместо неё надо выставлять ANDROID_SDK_ROOT. Стоп, она deprecated, и вместо неё надо выставлять... ANDROID_HOME. Ок.
👉 Надо ли устанавливать 32-битные версии библиотек? Хм.. Ничего не написано про то зачем они нужны, но кажется они нужны только для Android Studio. Убрал — ничего не сломалось.
Не всегда есть достаточное количество времени чтобы с этим всем разбираться, тем более если поддержка Dockerfile это не твоя прямая обязанность.
К сожалению, я не нашёл какого-то минимального образа, на базе которого можно было бы сделать свой образ не углубляясь во все эти дебри, поэтому — android-sdk:base в RedMadRobot/android-docker-images это как раз такой образ.
Стараюсь пояснять все неочевидные моменты комментами и в целом сохранять Dockerfile читаемым. Если вдруг что-то там не понятно — оставляйте комментарии к коммитам или открывайте issue.
Если всё-таки нужен образ, который может пережевать всё, рекомендую посмотреть на mingchen/docker-android-build-box.
#ciDocker-образа для сборки Android-приложений это какой-то ужас. Так я начал этот пост в прошлый понедельник, а потом пошёл за примерами "ужаса" и... застрял на неделю потому что:
- Во-первых, всё уже гораздо лучше чем было 3 года назад, когда я последний раз искал подходящий docker-образ
- Во-вторых, мои представления о "хорошем образе" немного устарели и образа могут быть ещё лучше 👇
Оказалось, что фича с адаптивными emoji, которые я исопльзовал в предыдущем посте, поддерживается не во всех клиентах. Я конечно завёл баги (macOS, Web K, Web Z), но хочется понять сколько людей затронула проблема (статистики по клиентам у меня нет).
За последние две недели вышло много крупных обновлений — 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