Техлид 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, — будем рады обсудить.

Комментарии (0)