Do not invoke slots in more than one place
mainmovableContentOf or custom layouts.repository·main·Indexed 19 days ago
https://github.com/slackhq/compose-lintsA collection of custom lint rules for Jetpack Compose designed to enforce best practices and prevent common mistakes. It includes checks for Composable naming conventions, CompositionLocal usage, modifier patterns (missing modifiers, reused modifiers, and missing default values), and content emission rules such as the 'emit XOR return' principle and the prevention of multiple top-level content emitters.
movableContentOf or custom layouts.Composable functions created solely for @Preview purposes should be marked as private. This prevents them from being used in actual UI code.
Compatibility Note: If using Detekt, this rule may conflict with Detekt's UnusedPrivateMember rule. To resolve this, configure Detekt's ignoreAnnotated to include ['Preview'].
A composable function should be cohesive and emit either 0 or 1 pieces of layout. Avoid writing composables that assume they are being called from a specific layout (like a Column) without explicitly enforcing that relationship.
Exceptions: You may emit multiple pieces of content if the function is tied to a specific scope via an extension receiver (e.g., ColumnScope.InnerContent()) or a context parameter (e.g., context(scope: ColumnScope)), provided the emitted calls actually use that context.
// BAD: Assumes it is called inside a Column
@Composable
private fun InnerContent() {
Text(...)
Image(...)
Button(...)
}
// GOOD: Emits a single cohesive layout node
@Composable
private fun InnerContent() {
Column {
Text(...)
Image(...)
Button(...)
}
}
// PERMITTED: Tied to a specific scope
@Composable
private fun ColumnScope.InnerContent() {
Text(...)
Image(...)
Button(...)
}The naming of @Composable functions depends on their return type:
Unit: Should start with an uppercase letter (treated as a declarative entity/class).Configuration: You can allow specific regex patterns for names using the allowed-composable-function-names option in lint.xml.
<!-- Configuration example in lint.xml -->
<issue id="ComposeNamingUppercase,ComposeNamingLowercase">
<option name="allowed-composable-function-names" value=".*Presenter" />
</issue>To follow the unidirectional data flow pattern (data flows down, events fire up), you should hoist state upwards. This makes composables stateless and easier to test.
Avoid the following anti-patterns:
ViewModels or Dependency Injection (DI) objects directly into composables.State<Foo> or MutableState<Bar> instances down into composables.Recommended pattern: Pass only the specific data required by the function and use lambdas for callbacks (events).
Follow these best practices for parameter ordering to improve usability:
Modifier as the final positional argument in most common cases.@Preview alternatives: use the suffix Previews (or Preview if there is only one).To maintain the pattern of 'state flowing down and events firing up', avoid passing mutable types like ArrayList<T>, MutableState<T>, or ViewModel as parameters to composables. Mutating a value inside a composable is an event that should be modeled via a lambda callback. Using mutable objects can prevent recomposition, meaning your UI won't automatically update when the value changes.
// Avoid this:
@Composable
fun MyComponent(list: ArrayList<String>) { ... }
// Do this:
@Composable
fun MyComponent(list: List<String>, onItemSelected: (String) -> Unit) { ... }For a composable to be restartable or skippable, all parameters—including the containing class or receiver (the 0th argument)—must be stable or immutable. Using an unstable receiver is typically a bug.
Note: This check (ComposeUnstableReceiver) is disabled by default because it is largely superseded by Strong Skipping. You must enable it manually in your lint configuration if you wish to use it.
Standard Kotlin collections (e.g., List<T>, Map<T>, Set<T>) are interfaces that do not guarantee immutability. Because the Compose compiler cannot verify if the underlying implementation is mutable, it treats these types as 'unstable', which can prevent composables from being skipped during recomposition.
This check is provided by the ComposeUnstableCollections rule and is disabled by default.
To resolve instability, use one of the following two methods:
Use kotlinx.collections.immutable to provide truly immutable implementations that the compiler can recognize.
@Immutable classWrap the collection in a data class annotated with @Immutable. Note that this only provides a promise of immutability to the compiler; the underlying list may still be mutable.
Related rule: ComposeUnstableCollections
// Option 1: Kotlinx Immutable Collections (Preferred)
val list: ImmutableList<String> = persistentListOf()
// Option 2: Wrapping in an annotated stable class
@Immutable
data class StringList(val items: List<String>)
val list: StringList = StringList(yourList)To use these custom lint checks for Jetpack Compose, add the compose-lint-checks dependency to your lintChecks configuration in your Gradle build file.
Note for non-Android projects: You must apply the com.android.lint Gradle plugin to use these checks.
dependencies {
lintChecks("com.slack.lint.compose:compose-lint-checks:<version>")
}