Когда K2?
Наверное, все уже увидели недавние новости о компиляторе K2 в Kotlin. Google написали в блоге о своём опыте, Compose добавили поддержку, а JetBrains обещали супер скорость.
Супер-скорость возможно и будет, но позже. Хотя уже есть нетерпеливые, которые уже прогнали и измерили улучшения или их отсутствие.
Меня в новом плагине подкупало то, что он даёт супер крутой функционал по написанию расширений для языка, которые автоматически подхватывает idea. "Просто подключить jar'ник в компилятор Kotlin в IDEA и все заведётся" - думал я.
С этими размышлениями взялся переписывать свои backend плагины на frontend. Да, не стоило мне ожидать, что это будет просто, потому что fir и ir абсолютно два разных семантических дерева. Очень часто помогал класс Fir2IrConverter, но чаще всего приходилось самому додумывать.
Из интересного то, что нельзя получить доступ к fir, а нужно получать разные обертки над ним с помощью extension функции, которые иногда приватные, а иногда публичные - не понял закономерности некоторых случаем. Опять же нельзя к самим декларациям обращаться, нужно делать это extension функции и получать символы декларации, которые содержат информацию, которую можно вытащить в плагине.
Понятно почему такое ограничение возникло. Во frontend компиляторе несколько фаз, в которых можно делать по сути что-то одно: добавлять супертипы, проверять наследников, добавлять аннотации или производить проверки кода. И на какой то фазе могут быть не доступны декларации, которые не зарезолвлены в предыдущих фазах.
Я писал проверки кода, поэтому предполагал, что ничего к этому моменту незарезолвлено не будет.
В итоге, подрубаю свой плагин - ничего не происходит. IDEA ничего не подсвечивает. Полез в Slack за ответами (почему раньше не сделал, не знаю) и оказывается, что на данный момент поддержка fir в IDEA экспериментальная, а чтобы подрубить плагин нужно собрать IDEA из исходников и подложить свой плагин...
Но вообще можно подключить не в IDEA, а напрямую в проект через Gradle, как и backend плагин в принципе. И тогда ошибки и предупреждения будут подхватываться в панели сборки. Из преимуществ над backend плагином показываются ворнинги и работает навигация к проблемному участку кода.
Да, это не совсем то, чего я хотел, и чтобы не оставлять дело незавершенным и набив руку на поиск соответствии между двумя семантическими деревьями, переписал с Fir на традиционный PSI для IDEA плагина)
В итоге, мне fir менее зашёл чем ir плагин, но его потенциал скоро будет заметен как добавят поддержку в IDEA, а пока довольствуемся очень нестабилбным API.
К слову, уже на этом этапе у fir больше документации, чем у ir)
https://github.com/JetBrains/kotlin/blob/master/docs/fir/fir-basics.md
https://github.com/JetBrains/kotlin/blob/master/docs/fir/fir-plugins.md
Наверное, все уже увидели недавние новости о компиляторе K2 в Kotlin. Google написали в блоге о своём опыте, Compose добавили поддержку, а JetBrains обещали супер скорость.
Супер-скорость возможно и будет, но позже. Хотя уже есть нетерпеливые, которые уже прогнали и измерили улучшения или их отсутствие.
Меня в новом плагине подкупало то, что он даёт супер крутой функционал по написанию расширений для языка, которые автоматически подхватывает idea. "Просто подключить jar'ник в компилятор Kotlin в IDEA и все заведётся" - думал я.
С этими размышлениями взялся переписывать свои backend плагины на frontend. Да, не стоило мне ожидать, что это будет просто, потому что fir и ir абсолютно два разных семантических дерева. Очень часто помогал класс Fir2IrConverter, но чаще всего приходилось самому додумывать.
Из интересного то, что нельзя получить доступ к fir, а нужно получать разные обертки над ним с помощью extension функции, которые иногда приватные, а иногда публичные - не понял закономерности некоторых случаем. Опять же нельзя к самим декларациям обращаться, нужно делать это extension функции и получать символы декларации, которые содержат информацию, которую можно вытащить в плагине.
Понятно почему такое ограничение возникло. Во frontend компиляторе несколько фаз, в которых можно делать по сути что-то одно: добавлять супертипы, проверять наследников, добавлять аннотации или производить проверки кода. И на какой то фазе могут быть не доступны декларации, которые не зарезолвлены в предыдущих фазах.
Я писал проверки кода, поэтому предполагал, что ничего к этому моменту незарезолвлено не будет.
В итоге, подрубаю свой плагин - ничего не происходит. IDEA ничего не подсвечивает. Полез в Slack за ответами (почему раньше не сделал, не знаю) и оказывается, что на данный момент поддержка fir в IDEA экспериментальная, а чтобы подрубить плагин нужно собрать IDEA из исходников и подложить свой плагин...
Но вообще можно подключить не в IDEA, а напрямую в проект через Gradle, как и backend плагин в принципе. И тогда ошибки и предупреждения будут подхватываться в панели сборки. Из преимуществ над backend плагином показываются ворнинги и работает навигация к проблемному участку кода.
Да, это не совсем то, чего я хотел, и чтобы не оставлять дело незавершенным и набив руку на поиск соответствии между двумя семантическими деревьями, переписал с Fir на традиционный PSI для IDEA плагина)
В итоге, мне fir менее зашёл чем ir плагин, но его потенциал скоро будет заметен как добавят поддержку в IDEA, а пока довольствуемся очень нестабилбным API.
К слову, уже на этом этапе у fir больше документации, чем у ir)
https://github.com/JetBrains/kotlin/blob/master/docs/fir/fir-basics.md
https://github.com/JetBrains/kotlin/blob/master/docs/fir/fir-plugins.md
🔥1
Mobius
Информации, которую нарыл по компиляторным плагинам и, в частности, по frontend плагинам оказалось достаточно, чтобы доклад прошёл ревью.
Из интересного, что проекты, которые буду приводить в качестве примеров использования компиляторных плагинов, содержат ещё idea и gradle плагины. Не хватает flipper плагина для полного набора)
https://mobiusconf.com/talks/0beebbbd16bf4358ab2a1b60cabf57a1/
Информации, которую нарыл по компиляторным плагинам и, в частности, по frontend плагинам оказалось достаточно, чтобы доклад прошёл ревью.
Из интересного, что проекты, которые буду приводить в качестве примеров использования компиляторных плагинов, содержат ещё idea и gradle плагины. Не хватает flipper плагина для полного набора)
https://mobiusconf.com/talks/0beebbbd16bf4358ab2a1b60cabf57a1/
🔥2👍1
Инкрементальная компиляции в Compose Compiler. Что может пойти не так?
Погружаясь в исходники компилятора компоуза никогда не думал, что может возникнуть проблема с инкрементальной компиляцией. Но как-то изучая причины, по которым Compose Compiler определяет стабильность типов и то, как использует их в дальнейшей генерации - начали появляться сомнения, что все действительно гладко.
Начну с известного: Compose Compiler любезно определяет стабильность классов даже если не проставить на самом деле это одно и то же на данный момент , проставляя аннотацию
Но этим польза
Дело в том, что
Соответственно, нужно размечать классы в других модулях, чтобы повторно не пытаться определять стабильность класса с меньшим количеством информации, чем оно было доступно при полной компиляции.
Но тут есть две проблемы (или особенности):
1)(сюрприз) и на нестабильные классы. А информация о нестабильности или стабильности будет лежать в поле
Чтобы понять к чему могут привести эти особенности, нужно копнуть немного глубже в понятие стабильности в компиляторе.
В Compose Compiler не ограничивается опеределением того, что класс стабильный или нестабильный. Есть ещё несколько вариантов описания стабильности, но концептуально можно разделить на три:
1., а как это произойдёт совсем другая история .
1. Есть используются generic'и
2. Если он из другого модуля в виде stub и помечен аннотацией
Например. Для мемоизации лямбд важно, чтобы класс был точно стабильным, а
В итоге, суммируя вышесказанное, получаем первый кейс с багом для инкрементальной компиляции.
Первый кейс. Класс лежит в другом модуле и он стабилен, но публичный, а значит пометился аннотацией
Второй кейс скорее положительный.
Допустим класс из другого модуля нестабильный и используется аргументах
Ситуация становится ещё более неочевидной, если учесть, что инкрементальная компиляция работает не только на уровне
Ну и не забываем, что
Погружаясь в исходники компилятора компоуза никогда не думал, что может возникнуть проблема с инкрементальной компиляцией. Но как-то изучая причины, по которым Compose Compiler определяет стабильность типов и то, как использует их в дальнейшей генерации - начали появляться сомнения, что все действительно гладко.
Начну с известного: Compose Compiler любезно определяет стабильность классов даже если не проставить
@Stable и @Immutable@StabilityInferred.Но этим польза
@StabilityInferred не заканчивается. Дело в том, что
gradle модули компилируются, грубо говоря, изолировано друг от друга, поэтому компиляторный плагин при инкрементальной компиляции не может получить абсолютно всю информацию о содержимом классов/функции из другого модуля. Например, не получится вытащить body или переменные сгенерированные (я проверял). То есть доступно только stub отображение класса. Соответственно, нужно размечать классы в других модулях, чтобы повторно не пытаться определять стабильность класса с меньшим количеством информации, чем оно было доступно при полной компиляции.
Но тут есть две проблемы (или особенности):
1)
@StabilityInferred вешается val $stable: Int
2) Аннотация вешается только на публичные классыЧтобы понять к чему могут привести эти особенности, нужно копнуть немного глубже в понятие стабильности в компиляторе.
В Compose Compiler не ограничивается опеределением того, что класс стабильный или нестабильный. Есть ещё несколько вариантов описания стабильности, но концептуально можно разделить на три:
1.
Stable
2. Unstable
3. Runtime stable
Если с первыми двумя все понятно, то третий говорит о том, что стабильность класса не определена и это скорее произойдет в runtimeRuntime stable может быть класс:1. Есть используются generic'и
2. Если он из другого модуля в виде stub и помечен аннотацией
@StabilityInferred
Но в Compose Compiler используют runtime stable в разных случаях и по разному. В одних случаях он трактуется как стабильный, а в других как нестабильный. Например. Для мемоизации лямбд важно, чтобы класс был точно стабильным, а
runtime stable трактуется как unstable. А вот для генерации возможности пропуска функции во время рекомпозиции важно, важно, чтоб класс не был unstable, то есть runtime stable в этом случае приравнивается к stable.В итоге, суммируя вышесказанное, получаем первый кейс с багом для инкрементальной компиляции.
Первый кейс. Класс лежит в другом модуле и он стабилен, но публичный, а значит пометился аннотацией
@StabilityInferred и для текущего модуля является runtime stable. Текущий модуль компилируется и перед тем как обернуть лямбду в remember, смотрит: а не захватывается ли чего нестабильного? В данном случае runtime stabe == unstable, поэтому лямбда не будет сохраняться между рекомпозициями, хотя при чистой сборке сохранялась. Второй кейс скорее положительный.
Допустим класс из другого модуля нестабильный и используется аргументах
@Composable функции, а значит он будет расцениваться как stable и сгенерируется возможность пропуска рекомпозиции, хотя её не было при чистой сборке.Ситуация становится ещё более неочевидной, если учесть, что инкрементальная компиляция работает не только на уровне
gradle модулей, но и kotlin файлов и кейсы выше будут так же верны и для случаев, когда функция и класс находятся в разных файлах.Ну и не забываем, что
@StabilityInferred проставляется только для публичных классов, а в модуле могут быть ещё internal классы, которые будут stub'ами при инкрементальной компиляции. А Compose Compiler помечает stub'ы как unstable, если нету аннотации. В итоге получаем ещё новые баги.👍7🔥1
Третий кейс. Класс стабильный, но
Четвёртый кейс. Та же ситуация с классом, но в случае мемоизации лямбды. Класс был стабилен при чистой сборке и мемоизация работала, а теперь точно не работает, потому что
Собрав все свои наработки сделал PR в Compose Compiler, а потом перенёс его в android review, из-за отсутствия активности на GitHub.
Но тут большое спасибо Андрею Шикову, который проревьюил. Большинство моих фиксов ломали бинарную совместимость, поэтому фиксить будет команда компилятора сама.
Первый фикс:
Теперь аннотация
Появится возможность самому регулировать степень мемоизации лямбд и сохранять их, даже если они захватывают что-то нестабильное. Кроме того, можно будет зафорсить пропускаемость функции, даже если параметры функции нестабильные. А потом отдельной аннотацией
Мне понравились такие классные решения, поэтому свернул свои изменения. Но эти изменения ещё не влиты и возможно что-то поменяется.
Но некоторые фиксы, которые лежали на поверхности, не касались инкрементальной компиляции и ничего особо не ломали, все таки смог влить.
Проблема генерации $stable для combined stable классов. Да, все это время эта штука не работала из-за небольшого бага)
Мемоизация работала для function reference, у которых extension reciever нестабильный. Это не совсем правильно, а про reciever я писал тут.
internal и лежит в другом файле, но при инкрементальной компиляции становится нестабильным, а функция, которая имела возможность пропуска рекомпозиции, большее её не имеет.Четвёртый кейс. Та же ситуация с классом, но в случае мемоизации лямбды. Класс был стабилен при чистой сборке и мемоизация работала, а теперь точно не работает, потому что
internal класс стал unstable.Собрав все свои наработки сделал PR в Compose Compiler, а потом перенёс его в android review, из-за отсутствия активности на GitHub.
Но тут большое спасибо Андрею Шикову, который проревьюил. Большинство моих фиксов ломали бинарную совместимость, поэтому фиксить будет команда компилятора сама.
Первый фикс:
Теперь аннотация
@StabilityInferred будет добавляться и для internal классов, а в добавок, чтобы не даунгрейдить определение стабильности класса из stable до runtime stable, добавится аннотация @KnownStability
Второй фикс (скорее фича):Появится возможность самому регулировать степень мемоизации лямбд и сохранять их, даже если они захватывают что-то нестабильное. Кроме того, можно будет зафорсить пропускаемость функции, даже если параметры функции нестабильные. А потом отдельной аннотацией
@NonSkippableComposable выборочно отключать эту штуку для конкретных функций.Мне понравились такие классные решения, поэтому свернул свои изменения. Но эти изменения ещё не влиты и возможно что-то поменяется.
Но некоторые фиксы, которые лежали на поверхности, не касались инкрементальной компиляции и ничего особо не ломали, все таки смог влить.
Проблема генерации $stable для combined stable классов. Да, все это время эта штука не работала из-за небольшого бага)
Combined stable - это такой тип стабильности, когда она определяется через стабильность нескольких классов, которые используются в одном классе. В $stable в таком случае не может лежать какое то определённое значение, а должно было генерироваться логическое сложение $stable полей используемых классов. Мемоизация работала для function reference, у которых extension reciever нестабильный. Это не совсем правильно, а про reciever я писал тут.
GitHub
Fix lambda and function reference memorization and internal classes loosing stability on incremental compilation. Fix $stable field…
Proposed Changes
I have started to fix bug(https://issuetracker.google.com/issues/298965625) and have founded other incorrect cases
Lambda memoization for RuntimeStable types
Problem: On incrementa...
I have started to fix bug(https://issuetracker.google.com/issues/298965625) and have founded other incorrect cases
Lambda memoization for RuntimeStable types
Problem: On incrementa...
👍5
Убийца Compose?
Совсем недавно узнал, что Huawei делают аналог Compose Multiplatform в виде нового декларативного фреймворка arkUI.
Я хотел попробовать, но на сайте все ну прям очень пусто. Просто немного скриншотов не сильно хорошего качества.
Ещё я узнал от HR из Huawei, что на проект они захантили лидов Compose Multiplatform из JB. Но пока отнесся к этому со скепсисом, так как никаких официальных заявлений нету и у этих лидов в linkedin все ещё висит, что место работы JB.
Изначально предположил, что это будет форк Compose Multiplatform. Например, DevEco Studio, который является форком то ли IntelliJ Idea, то ли Android Studio. Но HR говорит, что проект прям с нуля, но будет продолжать идеи Compose Multiplatform, хоть основным языком будет Js/Ts.
В общем жду. Будет интересно глянуть)
Совсем недавно узнал, что Huawei делают аналог Compose Multiplatform в виде нового декларативного фреймворка arkUI.
Я хотел попробовать, но на сайте все ну прям очень пусто. Просто немного скриншотов не сильно хорошего качества.
Ещё я узнал от HR из Huawei, что на проект они захантили лидов Compose Multiplatform из JB. Но пока отнесся к этому со скепсисом, так как никаких официальных заявлений нету и у этих лидов в linkedin все ещё висит, что место работы JB.
Изначально предположил, что это будет форк Compose Multiplatform. Например, DevEco Studio, который является форком то ли IntelliJ Idea, то ли Android Studio. Но HR говорит, что проект прям с нуля, но будет продолжать идеи Compose Multiplatform, хоть основным языком будет Js/Ts.
В общем жду. Будет интересно глянуть)
Harmonyos
ArkUI
ArkUI is a declarative UI development framework for building HarmonyOS application UIs. It offers simple UI information syntax, a wide range of UI components, and responsive live previewer, to boost your HarmonyOS app UI development efficiency by 30%. With…
🤔3
Небольшой квиз :) Как думаете, есть ли разница между лямбдой и ссылкой на функцию в качестве аргумента Composable функции?
Anonymous Quiz
25%
Нет
75%
Есть
Квиз закончился неоднозначно, поэтому попробую чуть подробнее описать, что будет в первом и втором случае.
Начнем с того, что
Разница будем именно в генерации кода для ссылки на функцию и лямбды, которые принимают аргументы. В данном случае
А вот лямбда будет обертнута в выражение.
Выражение:
В случае ссылки на функцию, если уберем
Почему же так? Просто в Compose три года назад написали так, что если ссылка на функцию принимает аргументы, то она не оборачивается в remember. И там же коммент, что надо бы это поправить в будущем, но оно пока не наступило)
Начнем с того, что
ViewModel стабильный, так как пустой и никакого стейта внутри не держит, а значит все оптимизации, которые генерирует, Compose будут работать.Разница будем именно в генерации кода для ссылки на функцию и лямбды, которые принимают аргументы. В данном случае
value: Int
Для ссылки на функцию ничего не сгенерируется.А вот лямбда будет обертнута в выражение.
rember(viewModel) { { value -> viewModel.doAction(value) }}
То есть в Compose, если лямбда захватывает переменные, то она будет оборачиваться в remember, если все захваченные переменные стабильные.Выражение:
val modifier = ModifierПревратится в:
val modifierProvider = { modifier.fillMaxSize() }
val modifier = ModifierТолько потому что
val modifierProvider = remember(modifier) { { modifier.fillMaxSize() } }
Modifier стабильный.В случае ссылки на функцию, если уберем
value: Int в doActionclass ViewModel {
fun doAction() {...}
}
То мемоизация уже будет работать. А код сгенерируется следующий:remember(viewModel, { viewModel::doAction }Почему же так? Просто в Compose три года назад написали так, что если ссылка на функцию принимает аргументы, то она не оборачивается в remember. И там же коммент, что надо бы это поправить в будущем, но оно пока не наступило)
👍2🗿1
Скинул свои находки коллегам, а в ответ мне скинули доклад уважаемых людей из ex-Twitter.
Там 40ой минуте(или на 129 слайде) ровно такой же пример, но рекомендуют они использовать ссылки на функцию вместо лямбд.
Показалось странным, но они ссылаются на статью еще одного уважаемого человека, где изначально была рекомендация. На эту же статью ссылаются еще две по оптимизациям в Compose: первая и вторая
Произошел какой-то скам или я попал в матрицу? :) Давайте разбираться и доказывать.
Там 40ой минуте(или на 129 слайде) ровно такой же пример, но рекомендуют они использовать ссылки на функцию вместо лямбд.
Показалось странным, но они ссылаются на статью еще одного уважаемого человека, где изначально была рекомендация. На эту же статью ссылаются еще две по оптимизациям в Compose: первая и вторая
Произошел какой-то скам или я попал в матрицу? :) Давайте разбираться и доказывать.
На первой картинке код, который сгенерирует Compose Compiler. Все получается ровно так, как описывалось ранее: лямбда оборачивается в remember, а ссылка на функцию нет.
Второй скриншот уже того, как это будет в байткоде (добавил еще аргумент со строкой для очевидности )
И наконец то, как это может повлиять на практите. Compose ссылки на функцию проверяет не через equlas, а ссылочно, поэтому мы получаем (третий скриншот):
1. лишние рекомпозиции
2. лишнее создания ссылки на функцию на каждую рекомпозицию
Второй скриншот уже того, как это будет в байткоде (
И наконец то, как это может повлиять на практите. Compose ссылки на функцию проверяет не через equlas, а ссылочно, поэтому мы получаем (третий скриншот):
1. лишние рекомпозиции
2. лишнее создания ссылки на функцию на каждую рекомпозицию
🔥8
А что там по issues?
Из свежего нашел такой - как low priority штуку все таки будут фиксить.
Почему тогда везде предлагают использовать ссылки на функцию?
У меня есть несколько объяснений этому:
1. Никто просто не проверил и лычки staffов в крупных компаниях хорошо сработали :)
2. Возможно раньше, когда в Kotlin Compiler был старый backend, это работало, а эта рекомендация тянется с тех пор. Но потом когда на стримах компилятор переписывали на IR, то этот случай пропустили?
Что же касается статьи, на которую много ссылок, то там же есть ссылка на github, где уже заведен одинокий issue на использование ссылки на функцию :)
Upd. Пофикшено тут, но теперь следите за context recievers или включайте strong skipping mode)
Из свежего нашел такой - как low priority штуку все таки будут фиксить.
Почему тогда везде предлагают использовать ссылки на функцию?
У меня есть несколько объяснений этому:
1. Никто просто не проверил и лычки staffов в крупных компаниях хорошо сработали :)
2. Возможно раньше, когда в Kotlin Compiler был старый backend, это работало, а эта рекомендация тянется с тех пор. Но потом когда на стримах компилятор переписывали на IR, то этот случай пропустили?
Что же касается статьи, на которую много ссылок, то там же есть ссылка на github, где уже заведен одинокий issue на использование ссылки на функцию :)
Upd. Пофикшено тут, но теперь следите за context recievers или включайте strong skipping mode)
👏4
С большим опозданием, но все же опубликовал исходный код с доклада.
Там самые разные компиляторные плагины, такие как:
- подсветка рекомпозиций
- логирование причин рекомпозиций
- удаление вызовов функции sourceInformation
- генерация/удаление/отображение testTag
- анализ стабильности параметров composable функций
Все компиляторные плагины подключаются к проекту как gradle plugin. Можно все по отдельности, а можно вместе - так удобнее настраивать.
В проверках стабильности параметров поддержаны самые последние обновления Compose Compiler, а именно возможность задавать в файле классы, которые надо пропустить в проверках. Подробнее писали тут.
Кроме этого есть ещё idea plugin.Правда в виде jar, так как получил отказ при попытке публикации в JetBrains Marketplace.
Он умеет отображать какие testTag будут сгенерированы компиляторным плагином, а так же проверять стабильность параметров функций и отображать ошибку в редакторе кода. Конечно, не на 100%, как это в компиляторном плагине сделано, но, думаю, в большинстве случаев правильно.
Ну и получил несколько просьб по поводу detekt правила для проверки параметров функций. Его тоже добавил. И там же есть возможность исключать классы, которые не нужно проверять.
Если будут проблемы или вопросы - пишите. Попробуем решить)
github.com/VKCOM/vkompose
Там самые разные компиляторные плагины, такие как:
- подсветка рекомпозиций
- логирование причин рекомпозиций
- удаление вызовов функции sourceInformation
- генерация/удаление/отображение testTag
- анализ стабильности параметров composable функций
Все компиляторные плагины подключаются к проекту как gradle plugin. Можно все по отдельности, а можно вместе - так удобнее настраивать.
В проверках стабильности параметров поддержаны самые последние обновления Compose Compiler, а именно возможность задавать в файле классы, которые надо пропустить в проверках. Подробнее писали тут.
Кроме этого есть ещё idea plugin.
Он умеет отображать какие testTag будут сгенерированы компиляторным плагином, а так же проверять стабильность параметров функций и отображать ошибку в редакторе кода. Конечно, не на 100%, как это в компиляторном плагине сделано, но, думаю, в большинстве случаев правильно.
Ну и получил несколько просьб по поводу detekt правила для проверки параметров функций. Его тоже добавил. И там же есть возможность исключать классы, которые не нужно проверять.
Если будут проблемы или вопросы - пишите. Попробуем решить)
github.com/VKCOM/vkompose
🔥28
Полуночные Зарисовки
Организация компонентов в Jetpack Compose
Telegraph
Организация компонентов в Jetpack Compose: Еще один вариант
Полная версия тут 5. Хранить данные в Modifier Чуть позже, просматривая различные репозитории в GitHub, наткнулся на ещё один вариант реализации компонентов, вдохновленный внутренней реализацией Modifier. В этом репозитории дизайн система довольно скудная…
🔥9