-
Notifications
You must be signed in to change notification settings - Fork 9
Home
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)) {
}
}
}
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) })
}
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
Checks that parameters of Composable functions have a correct order:
- Required parameters come first
- Optional parameters come after required
Non-compliant:
Header(
title: String,
enabled: Boolean = false,
description: String,
)
Compliant:
Header(
title: String,
description: String,
enabled: Boolean = false,
)
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())
}
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")
}
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")
}
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.
Finds and reports composable previews which are not marked as private
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())
}
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.
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' ]