Техлид Android-команды KODE Дмитрий Суздалев выпустил полноценный опенсорс-проект: набор улучшений для статического анализатора Kotlin-кода Detekt, которые обучают его проверять соблюдение различных правил при использовании библиотеки Jetpack Compose.
Набор правил попал в еженедельный Compose Newsletter, так что теперь самое время поделиться им с сообществом Хабра.
Detekt является довольно популярным средством для анализа качества кода. Вместе с неминуемым распространением Jetpack Compose спрос на специализированные правила для Detekt будет только расти.
Такой набор правил уже сформировался в нашей команде. Когда в процессе регулярных код-ревью приходилось снова и снова писать одни и те же замечания, касаемые стиля кода и наиболее распространенных ошибок, то логичным образом хотелось их автоматизировать и предупреждать ещё до код-ревью.
Идея оформить и опубликовать правила на Гитхабе пришла, когда на просторах Твиттера и Kotlin Slack стали задавать вопросы об их наличии.
Сейчас наши Сompose-правила вынесены в отдельный репозиторий. Ставьте
лайкизвёзды, подписывайтесь и читайте подробнее о правилах ниже.
1. Правило ReusedModifierInstance
Находит composable-функции, в теле которых параметр modifier
переиспользуется на разных уровнях вложенности. Как правило, такие ошибки нечаянно возникают при перемещении кода из функции в функцию или при оборачивании composable-вызовов в layout-ы. Практически всегда использование одного и того же инстанса modifier-а приводит к ошибкам рендеринга.
Например, представьте, что Column
в примере ниже когда-то был верхнеуровневым элементом в этой composeable-функции, но потом его обернули в Row
. При этом параметр modifier
забыли убрать и он остался уровнем ниже:
@Composable
fun MyComposable(modifier: Modifier) {
Row(modifier = Modifier.padding(30.dp)) {
Column(modifier = modifier.padding(20.dp)) {
}
}
}
@Composable
fun Content() {
MyComposable(modifier = Modifier.background(color = Color.Green))
}
Чтобы исправить эту ошибку, нужно оставить modifier
только на root-элементе данного composable:
@Composable
fun MyComposable(modifier: Modifier) {
Row(modifier = modifier.height(30.dp)) {
Column(modifier = Modifier.padding(20.dp)) {
}
}
}
2. Правило UnnecessaryEventHandlerParameter
Предлагает переместить передачу аргумента обработчика ошибок на уровень выше, что зачастую позволяет упростить composable-компоненты. Благодаря такому «вынесению» аргумента выше, уменьшается связность индивидуальных компонентов, они «меньше знают» о структуре данных с которыми они работают, оставляя диспатчинг аргументов родителю, что в свою очередь приводит к упрощению самого компонента.
В примере ниже, PrettyButton
без особых на то причин привязан к знанию о том, что Data
имеет поле id
, которое он использует в своём onClick
:
data class Data(id: Int, title: String)
fun PrettyButton(data: Data, onAction: (Int) -> Unit) {
Button(onClick = { onAction(data.id) })
}
fun Parent() {
val data = Data(id = 3, title = "foo")
PrettyButton(data = data, onAction = { id -> process(id) })
}
Это «знание» об id
на самом деле можно передвинуть в родителя, что не только упростит сам PrettyButton
, убрав лишнюю лямбда-обертку вокруг вызова onAction
, но также обеспечит более удобную работу с PrettyButton
при различных рефакторингах в дальнейшем. Вот здесь data.id
перенесён в родителя:
fun PrettyButton(data: Data, onAction: () -> Unit) {
Button(onClick = onAction)
}
fun Parent() {
val data = Data(id = 3, title = "foo")
PrettyButton(data = data, onAction = { process(data.id) })
}
3. Правило ComposableEventParameterNaming
Гарантирует, что все параметры обработчика событий composable-функций названы в одном и том же стиле, принятом в Compose, то есть они имеют префикс on
и не используют прошедшее время.
fun Button(click: () -> Unit) // ❌ wrong: missing "on"
fun Button(onClick: () -> Unit) // ✅ correct
fun Box(scroll: () -> Unit) // ❌ wrong: missing "on"
fun Box(onScroll: () -> Unit) // ✅ correct
fun Box(onScrolled: () -> Unit) // ❌ wrong: using past tense
fun Box(onScroll: () -> Unit) // ✅ correct
4. Правило ModifierHeightWithText
Предлагает использовать Modifier.heightIn()
вместо Modifier.height()
в лэйаутах, у которых есть дочерние элементы Text
. Это нужно для того, чтобы текст не обрезался, если он окажется длинным, не поместится в отведённую ширину и начнёт переноситься.
Row(modifier = Modifier.height(24.dp)) {
Text("hello")
}
Следует заменить на:
Row(modifier = Modifier.heightIn(min = 24.dp)) {
Text("hello")
}
5. Правило ModifierParameterPosition
Предлагает использовать корректное расположение параметра modifier
в списке параметров composable-функций: согласно принятому в Compose стилю, он должен идти после всех обязательных параметров и до опциональных параметров:
@Composable
fun Button(
modifier: Modifier = Modifier,
text: String,
onClick: () -> Unit,
arrangement: Arrangement = Vertical,
)
Следует заменить на:
@Composable
fun Button(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
arrangement: Arrangement = Vertical,
)
Обоснование: это позволит вызывать в короткой форме:
Button("Continue", onClick = { ... })
Title("Hello")
Иначе пришлось бы всегда использовать именованные параметры:
Button(text = "Continue", onClick = { ... })
Title(text = "Hello")
Библиотека android.compose.material
от Google следует этим договорённостям.
6. Правило PublicComposablePreview
Находит composable превью-функции, которые не отмечены как private
, и сообщает о них.
Заключение
На этом команда не планирует останавливаться: мы будем улучшать существующие и добавлять новые правила и доки. Пишите в комментариях, какие правила выработали вы за время работы с Jetpack Compose, — будем рады обсудить.