Skip to content

Code Convention

Lee Joonkyung edited this page Oct 4, 2023 · 12 revisions

This guidelines were written by referencing this.
You can confirm detail guidelines when you want to know more information.


1. Kotlin Style

Baseline style guidelines

  • Jetpack Compose framework development must follow the Kotlin Coding Conventions outlined at Docs as a baseline with the additional adjustments described below.

  • Jetpack Compose Library and app development should also follow this same guideline.


Singletons, constants, sealed class and enum class values

  • Jetpack Compose framework, Library, App development must name deeply immutable constants following the permitted object declaration convention of PascalCase as documented here as a replacement for any usage of CAPITALS_AND_UNDERSCORES.

  • Enum class values must also be named using PascalCase as documented in the same section.

Do

const val DefaultKeyName = "__defaultKey"

val StructurallyEqual: ComparisonPolicy = StructurallyEqualsImpl(...)

object ReferenceEqual : ComparisonPolicy {
    // ...
}

sealed class LoadResult<T> {
    object Loading : LoadResult<Nothing>()
    class Done(val result: T) : LoadResult<T>()
    class Error(val cause: Throwable) : LoadResult<Nothing>()
}

enum class Status {
    Idle,
    Busy
}

Don't

const val DEFAULT_KEY_NAME = "__defaultKey"

val STRUCTURALLY_EQUAL: ComparisonPolicy = StructurallyEqualsImpl(...)

object REFERENCE_EQUAL : ComparisonPolicy {
    // ...
}

sealed class LoadResult<T> {
    object Loading : LoadResult<Nothing>()
    class Done(val result: T) : LoadResult<T>()
    class Error(val cause: Throwable) : LoadResult<Nothing>()
}

enum class Status {
    IDLE,
    BUSY
}



2. Compose baseline

This section outlines guideline for @Composable functions and APIs that build on the Compose runtime capabilities. These guidelines apply to all Compose runtime-based APIs, regardless of the managed tree type.


Naming Unit @Compoable functions as entities

  • Jetpack Compose framework, Library, App development must name any function that returns Unit and bears the @Composable annotation using PascalCase, and the name must be that of a noun, not a verb or verb phrase, nor a nouned preposition, adjective or adverb.

  • This guideline applies whether the function emits UI elements or not.

Do

// This function is a descriptive PascalCased noun as a visual UI element
@Composable
fun FancyButton(text: String, onClick: () -> Unit) {

// This function is a descriptive PascalCased noun as a non-visual element
// with presence in the composition
@Composable
fun BackButtonHandler(onBackPressed: () -> Unit) {

Don't

// This function is a noun but is not PascalCased!
@Composable
fun fancyButton(text: String, onClick: () -> Unit) {

// This function is PascalCased but is not a noun!
@Composable
fun RenderFancyButton(text: String, onClick: () -> Unit) {

// This function is neither PascalCased nor a noun!
@Composable
fun drawProfileImage(image: ImageAsset) {

Naming @Composable functions that return values

  • Jetpack Compose framework, Library development must follow the standard Kotlin Coding Conventions for the naming of functions for any function annotated @Composable that returns a value other than Unit.

  • Jetpack Compose framework, Library development must not use the factory function exemption in the Kotlin Coding Conventions for the naming of functions for naming any function annotated @Composable as a PascalCase type name matching the function's abstract return type.

Do

// Returns a style based on the current CompositionLocal settings
// This function qualifies where its value comes from
@Composable
fun defaultStyle(): Style {

Don't

// Returns a style based on the current CompositionLocal settings
// This function looks like it's constructing a context-free object!
@Composable
fun Style(): Style {

Naming @Composable functions that remember {} the objects they return

  • Jetpack Compose framework, Library, App development must prefix any @Composable factory function that internally remember {}s and returns a mutable object with the prefix remember.

Do

// Returns a CoroutineScope that will be cancelled when this call
// leaves the composition
// This function is prefixed with remember to describe its behavior
@Composable
fun rememberCoroutineScope(): CoroutineScope {

Don't

// Returns a CoroutineScope that will be cancelled when this call leaves
// the composition
// This function's name does not suggest automatic cancellation behavior!
@Composable
fun createCoroutineScope(): CoroutineScope {

Naming CompositionLocals

  • Jetpack Compose framework and Library development must not name CompositionLocal keys using "CompositionLocal" or "Local" as a noun suffix. CompositionLocal keys should bear a descriptive name based on their value.

  • Jetpack Compose framework and Library development may use "Local" as a prefix for a CompositionLocal key name if no other, more descriptive name is suitable.

Do

// "Local" is used here as an adjective, "Theme" is the noun.
val LocalTheme = staticCompositionLocalOf<Theme>()

Don't

// "Local" is used here as a noun!
val ThemeLocal = staticCompositionLocalOf<Theme>()



3. Stable types

  • Jetpack Compose framework, Library, App development must ensure in custom implementations of .equals() for @Stable types that for any two references a and b of @Stable type T, a.equals(b) must always return the same value. This implies that any future changes to a must also be reflected in b and vice versa.

  • Jetpack Compose framework, Library development should correctly annotate @Stable and @Immutable types that they expose as part of their public API.

  • Jetpack Compose framework, Library development must not remove the @Stable or @Immutable annotation from a type if it was declared with one of these annotations in a previous stable release.

  • Jetpack Compose framework, Library development must not add the @Stable or @Immutable annotation to an existing non-final type that was available in a previous stable release without this annotation.


Emit XOR return a value

  • @Compose functions should either emit content into the composition or return a value, but not both. If a composable should offer additional control surfaces to its caller, those control surfaces or callbacks should be provided as parameters to the composable function by the caller.

Do

// Emits a text input field element that will call into the inputState
// interface object to request changes
@Composable
fun InputField(inputState: InputState) {
// ...

// Communicating with the input field is not order-dependent
val inputState = remember { InputState() }

Button("Clear input", onClick = { inputState.clear() })

InputField(inputState)

Don't

// Emits a text input field element and returns an input value holder
@Composable
fun InputField(): UserInputState {
// ...

// Communicating with the InputField is made difficult
Button("Clear input", onClick = { TODO("???") })
val inputState = InputField()



4. Compose UI API structure

  • Compose UI is a UI toolkit built on the Compose runtime.

  • A @Composable function that emits exactly one Compose UI tree node is called an element.


Elements return Unit

  • Elements must emit their root UI node either directly by calling emit() or by calling another Compose UI element function.
  • They must not return a value. All behavior of the element not available from the state of the composition must be provided by parameters passed to the element function.

Do

@Composable
fun FancyButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {

Don't

interface ButtonState {
    val clicks: Flow<ClickEvent>
    val measuredSize: Size
}

@Composable
fun FancyButton(
    text: String,
    modifier: Modifier = Modifier
): ButtonState {

Elements accept and respect a Modifier parameter

  • Element functions must accept a parameter of type Modifier. This parameter must be named "modifier" and must appear as the first optional parameter in the element function's parameter list.

  • Element functions must not accept multiple Modifier parameters.

  • Element functions may concatenate additional modifiers to the end of the received modifier parameter before passing the concatenated modifier chain to the Compose UI node they emit.

  • Element functions must not concatenate additional modifiers to the beginning of the received modifier paramter before passing the concatenated modifier chain to the Compose UI node they emit.

Do

@Composable
fun FancyButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) = Text(
    text = text,
    modifier = modifier.surface(elevation = 4.dp)
        .clickable(onClick)
        .padding(horizontal = 32.dp, vertical = 16.dp)
)

Compose UI layouts

  • Layout functions should use the name "content" for a @Composable function parameter if they accept only one @Composable function parameter.

  • Layout functions should use the name "content" for their primary or most common @Composable function parameter if they accept more than one @Composable function parameter.

  • Layout functions should place their primary or most common @Composable function parameter in the last parameter position to permit the use of Kotlin's trailing lambda syntax for that parameter.


Compose UI modifiers

  • A Modifier is an immutable, ordered collection of objects that implement the Modifier.Element interface

  • Modifier APIs must not expose their Modifier.Element interface implementation types. Modifier APIs must be exposed as factory functions following this style.

fun Modifier.myModifier(
    param1: ...,
    paramN: ...
): Modifier = then(MyModifierImpl(param1, ... paramN))
  • Jetpack Compose framework, Library development should use scoped modifier factory functions to provide parent data modifiers specific to a parent layout composable
@Stable
interface WeightScope {
    fun Modifier.weight(weight: Float): Modifier
}

@Composable
fun WeightedRow(
    modifier: Modifier = Modifier,
    content: @Composable WeightScope.() -> Unit
) {
// ...

// Usage:
WeightedRow {
    Text("Hello", Modifier.weight(1f))
    Text("World", Modifier.weight(2f))
}



5. Compose API design patterns

Prefer stateless and controlled @Composable functions

  • "stateless" refers to @Composable functions that retain no state of their own, but instead accept external state parameters that are owned and provided by the caller.

  • "Controlled" refers to the ides that the caller has full control over the state provided to the composable

Do

@Composable
fun Checkbox(
    isChecked: Boolean,
    onToggle: () -> Unit
) {
// ...

// Usage: (caller mutates optIn and owns the source of truth)
Checkbox(
    myState.optIn,
    onToggle = { myState.optIn = !myState.optIn }
)

Don't

@Composable
fun Checkbox(
    initialValue: Boolean,
    onChecked: (Boolean) -> Unit
) {
    var checkedState by remember { mutableStateOf(initialValue) }
// ...

// Usage: (Checkbox owns the checked state, caller notified of changes)
// Caller cannot easily implement a validation policy.
Checkbox(false, onToggled = { callerCheckedState = it })

Separate state and events

  • All individual events in a stream are assumed to be relevant and may build on one another. repeated equal events have meaning and therefore a registered observer must observe all events without skipping

  • Observable state raises change events when the state changes from one value to a new, unequal value. Observers of state changes must therefore be idempotent given the same state value the observer should produce the same result.

  • Compose operates on state as input, not events. Composable functions are state observers where both the function parameters and any mutableStateOf() value holders that are read during execution are inputs.


Hoisted state types

  • A pattern of stateless parameters and multiple event callback parameters will eventually reach a point of scale where it becomes unwieldy. As a composable function's parameter list grow it may become appropriate to factor a collection of state and callbacks into an interface, allowing a caller to provide a cohesive policy object as a unit.

Before

@Composable
fun VerticalScroller(
    scrollPosition: Int,
    scrollRange: Int,
    onScrollPositionChange: (Int) -> Unit,
    onScrollRangeChange: (Int) -> Unit
) {

After

@Stable interface VerticalScrollerState { var scrollPosition: Int var scrollRange: Int }

@Composable fun VerticalScroller( verticalScrollerState: VerticalScrollerState ) {


- Jetpack Compose framework, Library development should declare hoisted state types for collecting and grouping interrelated policy.

- Jetpack Compose framework, Library development should declare hoisted state types as @Stable and correctly implement the @Stable contract.

- Jetpack Compose framework and Library development should name hoisted state types that are specific to a given composable function as the composable function's name suffixed by, "State"

<br>

## Default policies through hoisted state objects
- By using Kotlin's default arguments, Compose's remember {} API, and the Kotlin "extension constructor" pattern, an API can provide a default state handling policy for simple usage while permitting more sophisticated usage when desired.

- Jetpack Compose framework, Library development should declare hoisted state types as interfaces instead of abstract or open classes if they are not declared as final classes.

- Jetpack Compose framework, Library development should provide default state implementations remembered as default arguments. State object may be required parameters if the composable cannot function if the state object is not configured by the caller.

- Jetpack Compose framework, Library development must not use null as a sentinel indicating that the composable function should internally remember {} its own state. 

### Do
```kotlin
@Composable
fun VerticalScroller(
    verticalScrollerState: VerticalScrollerState =
        remember { VerticalScrollerState() }
) {

Don't

// Null as a default can cause unexpected behavior if the input parameter
// changes between null and non-null.
@Composable
fun VerticalScroller(
    verticalScrollerState: VerticalScrollerState? = null
) {
    val realState = verticalScrollerState ?:
        remember { VerticalScrollerState() }

Extensibility of hosited state types

  • Jetpack Compose framework and Library development should declare hoisted state types as interfaces to permit custom implementations. If additional standard policy enforcement is necessary, consider an abstract class.

  • Jetpack Compose framework and Library development should offer a factory function for a default implementation of hoisted state types sharing the same name as the type.

@Stable
interface FooState {
    // ...
}

fun FooState(): FooState = FooStateImpl(...)

private class FooStateImpl(...) : FooState {
    // ...
}

// Usage
val state = remember { FooState() }