Twitter Compose Static Analysis Rules – Guidelines and Best Practices
Twitter’s Compose static‑analysis guide defines a comprehensive set of lint rules—covering state hoisting, proper use of remember, @Immutable annotations, stable collections, safe parameter passing, single‑layout emission, naming conventions for locals, previews and functions, and mandatory Modifier handling—to help large teams write clean, performant Jetpack Compose code.
This document introduces a set of custom static‑analysis rules for Jetpack Compose, created by Twitter to help large teams avoid common pitfalls when writing composable functions.
State‑related rules
1. State hoisting – Follow the unidirectional data‑flow principle (state flows down, events flow up). Keep most composables stateless by hoisting state to callers. See the official state‑hoisting guide . The rule is implemented in ComposeViewModelForwarding.kt (named vm-forwarding-check ).
2. Remember state – When creating a State instance with mutableStateOf (or similar), always wrap it with remember so that recomposition does not recreate a new state object. Implemented in ComposeRememberMissing.kt ( remember-missing-check ).
3. Use @Immutable – Annotate classes that are truly immutable with @Immutable to help the Compose compiler avoid unnecessary recompositions. See the Immutable documentation and composable‑metrics guide.
4. Avoid unstable collections – Kotlin collection interfaces ( List , Map , Set ) may hold mutable data, causing the compiler to treat them as unstable. Prefer immutable collection types such as ImmutableList (e.g., val list: ImmutableList<String> = persistentListOf() ) or wrap them in a class annotated with @Immutable :
@Immutable
data class StringList(val items: List
)
val list: StringList = StringList(yourList)For more details see the Jetpack Compose stability article and the Kotlinx immutable‑collections repository. The rule lives in ComposeUnstableCollections.kt ( unstable-collections ).
Composable‑function rules
5. Do not pass mutable state as parameters – State objects such as ViewModel , MutableState<T> , or ArrayList<T> should not be passed directly to composables. Instead, pass immutable data and callbacks. Implemented in ComposeMutableParameters.kt ( mutable-params-check ).
6. Emit layout OR return a value, not both – A composable should either emit UI or return a result, never both. See the Compose API guidelines. Implemented in ComposeMultipleContentEmitters.kt ( content-emitter-returning-values-check ).
7. Emit a single layout fragment – A composable should emit at most one layout node to keep it cohesive. An exception is allowed for scope‑extension functions (e.g., ColumnScope.InnerContent() ). Implemented in ComposeMultipleContentEmitters.kt ( multiple-emitters-check ).
8. Name CompositionLocals properly – Use the prefix Local followed by a descriptive noun (e.g., LocalUser ). Implemented in ComposeCompositionLocalNaming.kt ( compositionlocal-naming ).
9. Name multipreview annotations with the Previews suffix – Ensures developers recognise them as groups of @Preview annotations. Implemented in ComposePreviewNaming.kt ( preview-naming ).
10. Name composable functions correctly – Unit‑returning composables (treated as UI entities) should start with an uppercase letter; value‑returning composables should follow Kotlin function‑naming conventions (lowercase). Implemented in ComposeNaming.kt ( naming-check ).
11. Order parameters logically – Required parameters first, then optional ones (e.g., Modifier as the first optional parameter). Implemented in ComposeParameterOrder.kt ( param-order-check ).
12. Declare dependencies explicitly – Avoid obtaining ViewModel or CompositionLocal inside a composable; inject them via parameters with default values. See ComposeViewModelInjection.kt ( vm-injection-check ) and CompositionLocalAllowlist.kt ( compositionlocal-allowlist ).
13. Keep preview‑only composables private – Prevent accidental usage in production code. Implemented in ComposePreviewPublic.kt ( preview-public-check ).
Modifier‑related rules
14. Provide a Modifier parameter – Every public composable should expose a modifier: Modifier = Modifier parameter to allow callers to customize behavior. Implemented in ComposeModifierMissing.kt ( modifier-missing-check ).
15. Do not reuse the same Modifier instance across multiple children – Pass the received Modifier only to the root layout; child composables should use Modifier (or a new instance) directly. Implemented in ComposeModifierReused.kt ( modifier-reused-check ).
16. Give the Modifier parameter a default value – The default should be the empty Modifier and appear as the first optional parameter after required ones. Implemented in ComposeModifierWithoutDefault.kt ( modifier-without-default-check ).
17. Avoid building Modifiers with extension functions – Use Modifier.composed to limit recomposition to the Modifier itself rather than the whole composable tree. Implemented in ComposeModifierReused.kt (related to composed modifiers).
Conclusion
The rules above reflect Twitter’s accumulated experience with Compose and are intended to be used with linting tools such as ktlint and Detekt . Integration guides are available at https://twitter.github.io/compose-rules/ktlint and https://twitter.github.io/compose-rules/detekt .
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.