Skip to content
dimsuz edited this page Feb 27, 2024 · 6 revisions

Rule ReusedModifierInstance

Finds usages of the modifier parameter on non-top-level children of a composable function. This tends to happen during refactorings and often leads to incorrect rendering of a composable.

For example, imagine that Column here used to be a top composable, but then it got wrapped by the Row. But the modifier parameter was moved along with it and is now applied to the wrong composable:

@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))
}

This should be fixed by using modifier parameter on the Row instead:

@Composable
fun MyComposable(modifier: Modifier) {
  Row(modifier = modifier.height(30.dp)) {
    Column(modifier = Modifier.padding(20.dp)) {
    }
  }
}

Rule UnnecessaryEventHandlerParameter

Suggests hoisting event argument passing to the upper level which often simplifies individual composable components. This makes individual components less coupled to the structure of their parameters and leaves that to the parent, which in turn often leads to simplification of a composable.

For example here the PrettyButton is unnecessary coupled to the structure of Data — it extracts id field inside the 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) })
}

This "knowledge" of id can be moved to the parent which would not only simplify the PrettyButton by removing unnecessary lambda wrapper around onAction call, but this also makes it easier to work with PrettyButton later, during refactorings. Here the data.id is hoisted into the parent:

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) })
}

Rule ComposableEventParameterNaming

Ensures that all event handler parameters of composable functions are named in the same Compose-like style, i.e. they have on prefix and do not use past tense.

This rule suggests naming improvements

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

Rule ComposeableParametersOrdering

Checks that parameters of Composable functions have a correct order:

  1. Required parameters come first
  2. Optional parameters come after required

Non-compliant:

Header(
  title: String,
  enabled: Boolean = false,
  description: String,
)

Compliant:

Header(
  title: String,
  description: String,
  enabled: Boolean = false,
)

Rule ModifierDefaultValue

Checks that the modifier parameter of a Composable function has the correct default value.

Using a default value other than Modifier can lead to various non-obvious issues and inconveniences.

Non-compliant:

fun Content(modifier: Modifier = Modifier.fillMaxSize()) {
  Text("Greetings", modifier) // fillMaxSize will be ignored here
}

Compliant:

fun Content(modifier: Modifier = Modifier) {
  Text("Greetings", modifier.fillMaxSize())
}

Rule MissingModifierDefaultValue

Checks that the modifier parameter of a Composable function has a default value.

Non-compliant:

fun Content(modifier: Modifier) {
  Text("Greetings")
}

Compliant:

fun Content(modifier: Modifier = Modifier) {
  Text("Greetings")
}

Rule ModifierHeightWithText

Suggests using Modifier.heightIn() instead of Modifier.height() on a layouts which have Text children, so that if the text turns out to be long and would overflow and wrap, layout will not cut it off

Row(modifier = Modifier.height(24.dp)) {
  Text("hello")
}

with

Row(modifier = Modifier.heightIn(min = 24.dp)) {
  Text("hello")
}

Rule ModifierParameterPosition

Ensure that modifier is declared as a first parameter after required parameters and before optional parameters:

@Composable
fun Button(
  modifier: Modifier = Modifier,
  text: String,
  onClick: () -> Unit,
  arrangement: Arrangement = Vertical,
)

Should be replaced with:

@Composable
fun Button(
  text: String,
  onClick: () -> Unit,
  modifier: Modifier = Modifier,
  arrangement: Arrangement = Vertical,
)

Rationale: this would allow one to write a short form

Button("Continue", onClick = { ... })
Title("Hello")

otherwise it would be required to always use named parameters

Button(text = "Continue", onClick = { ... })
Title(text = "Hello")

Google's androidx.compose.material composables follow this convention.

Rule PublicComposablePreview

Finds and reports composable previews which are not marked as private

Rule TopLevelComposableFunctions

Checks that composable function is defined as a top-level function.

allowInObjects config property can be used to control if usage of composable functions in object is permitted.

Non-compliant code would look like this:

interface Screen {
  @Composable
  fun Content(modifier: Modifier = Modifier)
}

class ScreenImpl : Screen {
  @Composable
  override fun Content(modifier: Modifier) {
    Text("Greetings", modifier.fillMaxSize())
  }
}

And the compliant code is to have a top-level composable function:

fun ScreenContent(modifier: Modifier = Modifier) {
  Text("Greetings", modifier.fillMaxSize())
}

Rule ComposableFunctionName

The @Composable functions that return Unit should start with upper-case while the ones that return a value should start with lower case.

Examples of compliant and non-compliant code are given below.

Non-compliant:

@Composable
fun button() {
  …
}

Correct:

@Composable
fun Button() {
  …
}

Non-compliant:

@Composable
fun Value(): Int =

Compliant:

@Composable
fun value(): Int =

See also: Compose API guidelines.

Rule ConditionCouldBeLifted

Reports cases where a Compose layout contains a single conditional expression which could be "lifted" out of it.

For example in this code the if-condition could be moved outside of Column without any problems and this could potentially result in snappier rendering due to less composables having to be accounted for and/or recomposed:

Column {
  if(condition) {
    Row()
    Row()
  }
}

When moved out:

if(condition) {
  Column {
    Row()
    Row()
  }
}

Use ignoreCallsWithArgumentNames config option to specify argument names which (when present) will make this rule ignore and skip those calls:

// in detekt-config.yaml
ConditionCouldBeLifted:
  active: true
  ignoreCallsWithArgumentNames: [ 'modifier', 'contentAlignment' ]