Back to Androidx

Basic Patterns

docs/api_guidelines/compose_api_guidelines/basic_patterns.md

latest6.7 KB
Original Source

Basic Patterns {#basic-patterns}

This section covers the basic API patterns used throughout the compose libraries and lightly explains when each is applicable.

@Composable component {#component-pattern}

A @Composable component is defined as a @Composable function that returns Unit, emits a Layout, and accepts a Modifier.

Components:

  • MUST accept exactly one Modifier as the first default argument
  • MUST emit exactly one Layout
  • MAY draw something (directly, or via content parameters)
  • MAY accept user input
  • MAY accept any number other composables via slots (content)
kotlin
@Composable
fun ExampleComposable(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit,
) {
    // Box emits a layout
    // content may draw something or accept input
    Box(
        modifier = modifier.exampleDecorations(),
        content = content
    )
}

Modifier {#modifier-pattern}

A modifier is a wrapper around a single layout, and can be chained with other modifiers.

Modifiers passed to a component MUST apply to exactly one layout, and can perform any basic function of compose: Measure, Layout, Draw, Semantics, etc.

Developers typically pass modifiers using a fluent-style builder to customize a component.

kotlin
Component(
    modifier = Modifier
        .padding(...)
        .background(...)
)

Component or Modifier {#modifier-vs-component}

Components are the nouns of Compose, and named UI elements that describe a user-visible widget or layout MUST be components. For example Button, Text, Column and Box are all components.

Any feature that needs to emit different components over time MUST be a component.

Features that do not need to emit a new layout node, and only modifies exactly one layout and can be applied to any layout MAY be a modifier.

Features that are applied to arbitrary single layouts (e.g. padding, drawBehind) SHOULD be a Modifier.

DON’T

kotlin
@Composable
fun Padding(allSides: Dp, content: @Composable () -> Unit) {
    // impl
}

// usage
Padding(12.dp) {
    // What does padding mean with 2x children?
    UserCard()
    UserPicture()
}

Do:

kotlin
fun Modifier.padding(allSides: Dp): Modifier = // implementation

// usage
UserCard(modifier = Modifier.padding(12.dp))

Do

kotlin
@Composable
fun AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    // ... lots of details
    if (isVisibleOrAnimating) {
        AnimatedVisibilityImpl(
            content,
            visible,
            modifier.layout { /* details */ }
        )
    }
}

// usage: AnimatedVisibility has to have power to remove/add UserCard
// to hierarchy depending on the visibility flag
AnimatedVisibility(visible = false) {
    UserCard()
}

Remember factory {#remember-factory-pattern}

Used when typical API interactions involve a developer producing a state-object in composition.

If the following criteria are met a composable factory MAY be used for APIs where a state object is required as a parameter to a composable or modifier and it is likely to be created at the call-site.

At least one of the following should be true for all remember factories:

  1. Constructing an object in composition requires several CompositionLocal reads.
  2. Using the remember factory wires up rememberSavable
  3. Complex remember interaction that involves handling dispose.

Objects that can be constructed easily SHOULD expose a regular constructor only.

Remember factories with parameters fall into three categories:

  1. Simple keys – recreate the object when parameters change
  2. Param state changes - update state object when parameters change
  3. Parameter factories - pass a lambda producer instead of a parameter

You may prefer simple keys if:

  • Developers don't typically modify the state explicitly
  • It is difficult to design a param state change model

You may prefer to use the param state update model if:

  • Allocating a new object is prohibitively expensive
  • Developers are expected to modify the returned object, and will be surprised by the reallocation

You may prefer parameter factories if:

  • Reading the parameter can be done in a different restart scope
  • Reading the parameter may not happen in some branches
kotlin
// showing all three parameter options

@Composable
fun rememberItemReturned(param: Para): ItemReturned {
    // example of simple keys pattern
    return rememberSavable(param, saver = ItemReturnedSaver) {
        ItemReturned(param)
    }
}

@Composable
fun rememberItemReturned(param: Param): ItemReturned {
    // example of param state changes
    return rememberSavable(saver = ItemReturnedSaver) {
        ItemReturned()
    }.also {
        it.param = param
    }
}

@Composable
fun rememberItemReturned(paramProducer: () -> Param): ItemReturned {
    // example of parameter factory
    return rememberSavable(saver = ItemReturnedSaver) {
        ItemReturned(paramProducer)
    }
}

// always expose a non-composable constructor or factory, no matter what style
// of remember* you expose
class ItemReturned(param: Param) {

}

Slots {#slots-pattern}

Slots are @Composable lambda passed to a component. This allows developers calling to fully control the behavior of parts of the component, allowing component authors to focus on solving one problem.

@Composable Lambdas SHOULD follow these naming rules

  • N=1 composable lambda: called content and is a trailing lambda.
  • N>1 composable lambdas one is "main" content: the "main" content should be trailing and called content, other lambdas are optional (nullable).
  • N>1 composable lambdas no priority: no trailing lambda, none are called content. Multiple lambdas are required (non-null)

Slots SHOULD be nullable when providing a value to the slot changes the behavior of the composable (e.g. padding changes).

kotlin
@Composable
fun Tab(
    ....
    modifier: Modifier = Modifier
    text: @Composable (() -> Unit)? = null,
    icon: @Composable (() -> Unit)? = null,
    //... more non-slot params ...
)

@Composable
public fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    // ...
    content: @Composable BoxScope.() -> Unit,
) =

Default objects {#default-pattern}

Default objects expose default arguments and constants used by components as public API, this allows developers to easily wrap the component in a new component with the exact same default behavior.

A default object is just an object with getters, factory methods, composable getters, and composable factories attached. All public methods should be useful for developers to implement workalike components properly.