compose-lints

repository·main·Indexed 19 days ago

https://github.com/slackhq/compose-lints

A 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.

Tokens
7.5K
Snippets
18
Records
41
Agent score
66%

What's inside compose-lints

  1. Do not invoke slots in more than one place

    main
    Slot parameters (content lambdas) should be invoked in exactly one place, or not at all. This ensures that the internal state of the slot is preserved during recomposition. If you need to reuse content, consider using movableContentOf or custom layouts.
  2. Restrict visibility of Preview composables

    main

    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'].

  3. Do not emit multiple pieces of content

    main

    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(...)
    }
  4. Name @Composable functions properly

    main

    The naming of @Composable functions depends on their return type:

    • Returning Unit: Should start with an uppercase letter (treated as a declarative entity/class).
    • Returning a value: Should start with a lowercase letter (following standard Kotlin function naming conventions).

    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>
  5. Hoist state in Composables

    main

    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:

    • Passing ViewModels or Dependency Injection (DI) objects directly into composables.
    • Passing 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).

  6. Order @Composable parameters properly

    main

    Follow these best practices for parameter ordering to improve usability:

    1. Mandatory parameters first, followed by optional parameters (those with default values).
    2. Modifiers should occupy the first optional parameter slot. This allows developers to provide a Modifier as the final positional argument in most common cases.
  7. Do not use inherently mutable types as parameters

    main

    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) { ... }
  8. Do not emit content and return a result

    main
    Composable functions should follow the 'emit XOR return' principle: they should either emit layout content OR return a value, but not both. If a composable needs to provide control surfaces to a caller, those should be passed in as parameters (e.g., callbacks).
  9. Avoid unstable receivers in Composables

    main

    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.

  10. Avoid using unstable collections in Compose

    main

    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:

    1. Use Kotlinx Immutable Collections (Preferred)

    Use kotlinx.collections.immutable to provide truly immutable implementations that the compiler can recognize.

    2. Wrap collections in an @Immutable class

    Wrap 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)
  11. Install Compose Lints

    main

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