API Guidelines for @Composable components in Jetpack Compose

Last updated: July 19, 2023

Set of guidelines and recommendations for building scalable and user-friendly @Composable components.

The requirement level of each of these guidelines is specified using the terms set forth in RFC2119 for each of the following developer audiences. If an audience is not specifically named with a requirement level for a guideline, it should be assumed that the guideline is OPTIONAL for that audience.

Jetpack Compose framework development

Contributions to the androidx.compose libraries and tools generally follow these guidelines to a strict degree to promote consistency, setting expectations and examples for consumer code at all layers.

Library development based on Jetpack Compose

It is expected and desired that an ecosystem of external libraries will come to exist that target Jetpack Compose, exposing a public API of @Composable functions and supporting types for consumption by apps and other libraries. While it is desirable for these libraries to follow these guidelines to the same degree as Jetpack Compose framework development would, organizational priorities and local consistency may make it appropriate for some purely stylistic guidelines to be relaxed.

App development based on Jetpack Compose

App development is often subject to strong organizational priorities and norms and requirements to integrate with existing app architecture. This may call for not only stylistic deviation from these guidelines but structural deviation as well. Where possible, alternative approaches for app development will be listed in this document that may be more appropriate in these situations.

Table of content

Note on vocabulary in this doc

@Composable component - A @Composable function that returns Unit and emits the UI when it is composed in a hierarchy (later: component).

Developer - a person who creates a component that is to be used by a user in an application or in another component library.

User - the user of the component - a person who uses the component in a composable hierarchy to show some ui to the end-user.

End-user - the person who will use the application created by the user of your component.

These guidelines outline the best practices for developing UI components using Jetpack Compose. Best practices ensure that the API of the components is:

  • Scalable long term: the author is able to evolve the API to cause the least amount of friction to users.
  • Consistent across other components: developers can use existing knowledge and patterns to work with new components that are created by different authors.
  • Guide developers towards the happy path: components will encourage the right practices and usages and disallow the incorrect usage where possible.

Before you create a component

When creating a new component:

  • Make sure there’s a single problem the component solves. Split components into subcomponents and building blocks until each solves a single problem users have.
  • Make sure you need a component and it brings value that justifies the long term support and evolution of its APIs. Often developers might find that it is easier for users to write the component code themselves so they can adjust later.

Component’s purpose

Consider the value a new component adds and the problem it solves. Each component should solve only one problem, and each problem should be solved in one place. If your component solves more than one problem look for opportunities to split it into layers or subcomponents. With the benefit of smaller, concise and use case targeted API comes the easy of use and clear understanding of the component contract.

Lower level building blocks and components usually add certain new single functionality and are easy to combine together. Higher level components serve a purpose of combining building blocks to provide an opinionated, ready to use behavior.

DON’T

// avoid multipurpose components: for example, this button solves more than 1 problem
@Composable
fun Button(
    // problem 1: button is a clickable rectangle
    onClick: () -> Unit = {},
    // problem 2: button is a check/uncheck checkbox-like component
    checked: Boolean = false,
    onCheckedChange: (Boolean) -> Unit,
) { ... }

Do:

@Composable
fun Button(
    // problem 1: button is a clickable rectangle
    onClick: () -> Unit,
) { ... }

@Composable
fun ToggleButton(
    // problem 1: button is a check/uncheck checkbox-like component
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit,
) { ... }

Component layering

When creating components, provide various layers of single purpose building blocks first that are needed for the component to work. Increase level of opinion ingrained and reduce the amount of customisations as you go from low level APIs to higher level. Higher level components should provide more opinionated defaults and fewer customisation options.

@Composable component creation was designed to be a low-effort operation in Compose so that users can create their own single purpose components and adjust them as needed.

Do:

// single purpose building blocks component
@Composable
fun Checkbox(...) { ... }

@Composable
fun Text(...) { ... }

@Composable
fun Row(...) { ... }

// high level component that is more opinionated combination of lower level blocks
@Composable
fun CheckboxRow(...) {
    Row {
        Checkbox(...)
        Spacer(...)
        Text(...)
    }
}

Do you need a component?

Question the need for creating the component in the first place. With high-level components that can be combined from building blocks, there has to be a strong reason for it to exist. Lower level components should solve a real problem that users have.

Try to create a Component from the publicly available building blocks. This provides the sense of what it feels like to be a developer who needs your component. If it looks simple, readable and doesn’t require hidden knowledge to make - this means users can do it themselves.

Consider the value your component brings to users if they choose it over doing it themselves. Consider the burden a component puts on a user who would need to learn new APIs to use them.

For example, a developer wants to create a RadioGroup component. In order to accommodate various requirements such as vertical and horizontal layouts, different types of data and decorations, the API might look like this:

@Composable
fun <T> RadioGroup(
    // `options` are a generic type
    options: List<T>,
    // horizontal or vertical
    orientation: Orientation,
    // some adjustments around content layout
    contentPadding: PaddingValues,
    modifier: Modifier = Modifier,
    optionContent: @Composable (T) -> Unit
) { ... }

While doing this, look first at how users would write it themselves using the available building blocks:

// Modifier.selectableGroup adds semantics of a radio-group like behavior
// accessibility services will treat it as a parent of various options
Column(Modifier.selectableGroup()) {
   options.forEach { item ->
       Row(
           modifier = Modifier.selectable(
               selected = (select.value == item),
               onClick = { select.value = item }
           ),
           verticalAlignment = Alignment.CenterVertically
       ) {
           Text(item.toString())
           RadioButton(
               selected = (select.value == item),
               onClick = { select.value = item }
           )
       }
   }
}

Now, developers should make a conscious decision on whether the RadioGroup API is worth it. In this particular example, users utilize familiar building blocks such as Row, Text and other basic tools. They also gain the flexibility to define the layouts needed or add any decorations and customisations. The case might be made to not to introduce any RadioGroup APIs at all.

Shipping a component is costly, involving at least the development, testing, long term support and subsequent evolution of the API.

Component or Modifier

Make a component if it has a distinct UI that cannot be applied to other components or if the component wants to make structural changes in the UI (add/remove other components).

Make the feature to be a Modifier instead if the bit of functionality can be applied to any arbitrary single component to add extra behavior. This is especially important when the functionality has undefined behavior when applied to a few UI components at the same time.

DON’T

@Composable
fun Padding(allSides: Dp) {
    // impl
}

// usage
Padding(12.dp) {
    // 1. Is it a padding around both card and picture or for each one?
    // 2. What are the layout expectations for card and picture?
    // 3. What if there is no content (no card and picture at all)?
    UserCard()
    UserPicture()
}

Do:

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

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

If the bit of functionality can be applied to any composable, but it has to alter the hierarchy of composables, it has to be a Component, since Modifiers cannot change the hierarchy:

Do

@Composable
fun AnimatedVisibility(
    visibile: Boolean,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    // impl
}

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

Name of a Component

Please, refer to the corresponding Compose API guidelines section for naming conventions. However, there are more detailed considerations to keep in mind.

Jetpack Compose framework development MUST follow the rules in this section.

Library development MUST follow the section below.

App development MAY follow the rules below.

BasicComponent vs Component

Consider Basic* prefix for components that provide barebones functionality with no decoration and/or with no design-system based visual opinions. This is a signal that users are expected to wrap it in their own decoration, as the component is not expected to be used as-is. As a counterpart to that, Component name without a prefix can represent components that are ready to use and are decorated according to some design specification.

Do:

// component that has no decoration, but basic functionality
@Composable
fun BasicTextField(
    value: TextFieldValue,
    onValueChange: (TextFieldValue) -> Unit,
    modifier: Modifier = Modifier,
    ...
)

// ready to use component with decorations
@Composable
fun TextField(
    value: TextFieldValue,
    onValueChange: (TextFieldValue) -> Unit,
    modifier: Modifier = Modifier,
    ...
)

Design, Usecase or Company/Project specific prefixes

Avoid CompanyName (GoogleButton) or Module (WearButton) prefixes where possible and consider use-case or domain specific names if needed to. If the component you are building is a part of component library built using compose-foundation or compose-ui building blocks as a basis, the majority of the non-prefixed names should be available to developers without clashes: com.companyname.ui.Button or com.companyname.ui.Icon. Simple names make sure these components feel first-class when used.

If wrapping existing components or building on top of another design system, consider names that are derived from the use case first: ScalingLazyColumn, CurvedText. If impossible or the use case clashes with the existing component, module/library prefix can be used e.g. GlideImage.

If your design system specification introduces a number of similar components with different appearances, consider using specification prefixes: ContainedButton, OutlinedButton, SuggestionChip, etc. Using prefixes helps you avoid “style” patterns and keep the API simple. See “ComponentColor/ComponentElevation” section for more details.

If you have a set of components with prefixes, consider choosing the default component, which is the one most likely to be used, and keep it without the prefix.

Do

// This button is called ContainedButton in the spec
// It has no prefix because it is the most common one
@Composable
fun Button(...) {}

// Other variations of buttons below:
@Composable
fun OutlinedButton(...) {}

@Composable
fun TextButton(...) {}

@Composable
fun GlideImage(...) {}

Also do (if your library is based on compose-foundation)

// package com.company.project
// depends on foundation, DOES NOT depend on material or material3

@Composable
fun Button(...) {} // simple name that feel like a first-class button

@Composable
fun TextField(...) {} // simple name that feel like a first-class TF

Component dependencies

Jetpack Compose framework development MUST follow the rules in this section.

Library development SHOULD follow the section below.

App development MAY follow the rules below.

Prefer multiple components over style classes

Express dependencies in a granular, semantically meaningful way. Avoid grab-bag style parameters and classes, akin to ComponentStyle or ComponentConfiguration.

When a certain subset of components of the same type need to have the same configurations or stylistical visual appearance, users should be encouraged to create their own semantically meaningful version of a component. This can be done either by wrapping the component or forking it and using lower-level building blocks. This is the component developer’s responsibility to make sure that both of those ways are low cost operations.

Instead of relying on the ComponentStyle to specify different component variations in the component library, consider providing separate @Composable functions named differently to signify the difference in styling and use cases for those components.

DON’T

// library code
class ButtonStyles(
    /* grab bag of different parameters like colors, paddings, borders */
    background: Color,
    border: BorderStroke,
    textColor: Color,
    shape: Shape,
    contentPadding: PaddingValues
)

val PrimaryButtonStyle = ButtonStyle(...)
val SecondaryButtonStyle = ButtonStyle(...)
val AdditionalButtonStyle = ButtonStyle(...)

@Composable
fun Button(
    onClick: () -> Unit,
    style: ButtonStyle = SecondaryButtonStyle
) {
    // impl
}

// usage
val myLoginStyle = ButtonStyle(...)
Button(style = myLoginStyle)

Do:

// library code
@Composable
fun PrimaryButton(
    onClick: () -> Unit,
    background: Color,
    border: BorderStroke,
    // other relevant parameters
) {
    // impl
}

@Composable
fun SecondaryButton(
    onClick: () -> Unit,
    background: Color,
    border: BorderStroke,
    // other relevant parameters
) {
    // impl
}

// usage 1:
PrimaryButton(onClick = { loginViewModel.login() }, border = NoBorder)
// usage 2:
@Composable
fun MyLoginButton(
    onClick: () -> Unit
) {
    // delegate to and wrap other components or its building blocks
    SecondaryButton(
        onClick,
        background = MyLoginGreen,
        border = LoginStroke
    )
}

Explicit vs implicit dependencies

Prefer explicit inputs and configuration options in your components, such as function parameters. Explicit inputs for the component make it easy to predict the component's behavior, adjust it, test and use.

Avoid implicit inputs provided via CompositionLocal or other similar mechanisms. Those inputs add complexity to the components and every usage of it and make it hard to track where customisation comes from for users. To avoid implicit dependencies, make it easy for users to create their own opinionated components with a subset of explicit inputs they wish to customize.

DON’T

// avoid composition locals for component specific customisations
// they are implicit. Components become difficult to change, test, use.
val LocalButtonBorder = compositionLocalOf<BorderStroke>(...)

@Composable
fun Button(
    onClick: () -> Unit,
) {
    val border = LocalButtonBorder.current
}

Do:

@Composable
fun Button(
    onClick: () -> Unit,
    // explicitly asking for explicit parameter that might have
    // reasonable default value
    border: BorderStroke = ButtonDefaults.borderStroke,
) {
    // impl
}

Consider using CompositionLocal to provide a global app or screen styling if needed. For example, design theming or typography in the material library can be implicitly specified for the whole app or screen. When doing so, make sure that those CompositionLocals are being read in the default expressions on the component parameters, so users can override them.

Since those objects rarely change and cover big subtrees of components of different kinds, the flexibility of app-wide customisation is usually worth the aforementioned downsides of the implicit inputs. In cases like this, components should be discouraged to read this CompositionLocal in implementation and instead read it in the default expressions, so it is easy to override when customizing or wrapping the component.

DON’T

// this is ok: theme is app global, but...
class Theme(val mainAppColor: Color)
val LocalAppTheme = compositionLocalOf { Theme(Color.Green) }

@Composable
fun Button(
    onClick: () -> Unit,
) {
    // reading theme in implementation makes it impossible to opt out
    val buttonColor = LocalAppTheme.current.mainAppColor
    Box(modifier = Modifier.background(buttonColor)) { ... }
}

Do:

// this is ok: theme is app global
class Theme(val mainAppColor: Color)
val LocalAppTheme = compositionLocalOf { Theme(Color.Green) }

@Composable
fun Button(
    onClick: () -> Unit,
    // easy to see where the values comes from and change it
    backgroundColor: Color = LocalAppTheme.current.mainAppColor
) {
    Box(modifier = Modifier.background(backgroundColor)) { ... }
}

There’s a blogpost published that describes the reasoning in depth in the chapter “Maintaining API consistency”.

Component parameters

Set of considerations regarding parameters of @Composable component.

Jetpack Compose framework development MUST follow the rules in this section below.

Compose library development SHOULD follow the rules in the sections below.

App development SHOULD follow.

Parameters vs. Modifier on the component

Do not introduce optional parameters that add optional behavior that could otherwise be added via Modifier. Parameters should allow to set or customize the behavior that exists internally in the component.

DON’T:

@Composable
fun Image(
    bitmap: ImageBitmap,
    // not core functionality, click can be added via Modifier.clickable
    onClick: () -> Unit = {},
    modifier: Modifier = Modifier,
    // can be specified via `Modifier.clip(CircleShape)`
    clipToCircle: Boolean = false
)

Do:

@Composable
fun Button(
    onClick: () -> Unit,
    // modifier param specified so that width, padding etc can be added
    modifier: Modifier = Modifier,
    // button is a colored rect that clicks, so background
    // considered as a core functionality, OK as a param
    backgroundColor: Color = MaterialTheme.colors.primary
)

modifier parameter

Every component that emits UI should have a modifier parameter. Make sure that modifier parameter:

  • Has the type Modifier.
    • Type Modifier ensures that any Modifier can be passed to the component.
  • Is the first optional parameter.
    • If a component has non-zero default size - modifier should be optional, since the component is self sufficient. For components with zero default size modifier parameter can be a required param.
    • Since the modifier is recommended for any component and is used often, placing it first ensures that it can be set without a named parameter and provides a consistent place for this parameter in any component.
  • Has a no-op default value Modifier.
    • No-op default value ensures that no functionality will be lost when users provide their own modifiers for the component.
  • Is the only parameter of type Modifier in the parameter list.
    • Since modifiers intend to modify the external behaviors and appearance of the component, one modifier parameter should be sufficient. Consider asking for specific parameters or reconsidering the layering of the component (e.g. braking component into two) instead.
  • Is applied once as a first modifier in the chain to the root-most layout in the component implementation.
    • Since modifiers intend to modify the external behaviors and appearance of the component, they must be applied to the outer-most layout and be the first modifiers in the chain. It is ok to chain other modifiers to the modifier that is passed as a parameter.

Why? Modifiers are the essential part of compose, users have expectations about their behavior and API. Essentially, modifiers provide a way to modify the external component behavior and appearance, while component implementation will be responsible for the internal behavior and appearance.

DON’T:

@Composable
fun Icon(
    bitmap: ImageBitmap,
    // no modifier parameter
    tint: Color = Color.Black
)

DON’T:

@Composable
fun Icon(
    bitmap: ImageBitmap,
    tint: Color = Color.Black,
    // 1: modifier is not the first optional parameter
    // 2: padding will be lost as soon as the user sets its own modifier
    modifier: Modifier = Modifier.padding(8.dp)
)

DON’T:

@Composable
fun CheckboxRow(
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit,
    // DON'T - modifier is intended to specify the external behavior of
    // the CheckboxRow itself, not its subparts. Make them slots instead
    rowModifier: Modifier = Modifier,
    checkboxModifier: Modifier = Modifier
)

DON’T:

@Composable
fun IconButton(
    buttonBitmap: ImageBitmap,
    modifier: Modifier = Modifier,
    tint: Color = Color.Black
) {
    Box(Modifier.padding(16.dp)) {
        Icon(
            buttonBitmap,
            // modifier should be applied to the outer-most layout
            // and be the first one in the chain
            modifier = Modifier.aspectRatio(1f).then(modifier),
            tint = tint
        )
    }
}

Do:

@Composable
fun IconButton(
    buttonBitmap: ImageBitmap,
    // good: first optional parameter, single of its kind
    modifier: Modifier = Modifier,
    tint: Color = Color.Black
) {
    // good: applied before other modifiers to the outer layout
    Box(modifier.padding(16.dp)) {
        Icon(buttonBitmap, modifier = Modifier.aspectRatio(1f), tint = tint)
    }
}

Also Do:

@Composable
fun ColoredCanvas(
    // ok: canvas has no intrinsic size, asking for size modifiers
    modifier: Modifier,
    color: Color = Color.White,
    ...
) {
    // good: applied before other modifiers to the outer layout
    Box(modifier.background(color)) {
        ...
    }
}

Parameters order

The order of parameters in a component must be as follows:

  1. Required parameters.
  2. Single modifier: Modifier = Modifier.
  3. Optional parameters.
  4. (optional) trailing @Composable lambda.

Why? Required parameters indicate the contract of the component, since they have to be passed and are necessary for the component to work properly. By placing required parameters first, API clearly indicates the requirements and contract of the said component. Optional parameters represent some customisation and additional capabilities of the component, and don’t require immediate attention of the user.

Explanation for the order of the parameters:

  1. Required parameters. Parameters that don’t have default values and the user is required to pass the values for those parameters in order to use the components. Coming first, they allow users to set them without using named parameters.
  2. modifier: Modifier = Modifier. Modifiers should come as a first optional parameter in a @composable function. It must be named modifier and have a default value of Modifier. There should be only one modifier parameter and it should be applied to the root-most layout in the implementation. See “modifier parameter” section for more information.
  3. Optional parameters. Parameters that have default values that will be used if not overridden by the user of the component. Coming after required parameters and a modifier parameter, they do not require the user to make an immediate choice and allow one-by-one override using named parameters.
  4. (optional) trailing @Composable lambda representing the main content of the component, usually named content. It can have a default value. Having non-@composable trailing lambda (e.g. onClick) might be misleading as it is a user expectation to have a trailing lambda in a component to be @Composable. For LazyColumn and other DSL-like exceptions, it is ok to have non-@composable lambda since it still represents the main content.

Think about the order of parameters inside the “required” and “optional” subgroups as well. Similar to the split between required and optional parameters, it is beneficial for the reader and user of the API to see the data, or “what” part of the component first, while metadata, customisation, the “how” of the component should come after.

It makes sense to group parameters semantically within the required or optional groups. If you have a number of color parameters (backgroundColor and contentColor), consider placing them next to each other to make it easy for the user to see customisation options.

Do

@Composable
fun Icon(
    // image bitmap and contentDescription are required
    // bitmap goes first since it is the required data for the icon
    bitmap: ImageBitmap,
    // contentDescription follows as required, but it is a "metadata", so
    // it goes after the "data" above.
    contentDescription: String?,
    // modifier is the first optional parameter
    modifier: Modifier = Modifier,
    // tint is optional, default value uses theme-like composition locals
    // so it's clear where it's coming from and to change it
    tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
)

Do

@Composable
fun LazyColumn(
    // no required parameters beyond content, modifier is the first optional
    modifier: Modifier = Modifier,
    // state is important and is a "data": second optional parameter
    state: LazyListState = rememberLazyListState(),
    contentPadding: PaddingValues = PaddingValues(0.dp),
    reverseLayout: Boolean = false,
    // arrangement and alignment go one-by-one since they are related
    verticalArrangement: Arrangement.Vertical =
        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
    userScrollEnabled: Boolean = true,
    // trailing lambda with content
    content: LazyListScope.() -> Unit
)

Nullable parameter

Make conscious choices between the semantical meaning of the parameter or its absence. There’s a difference between default value, empty value and absent value. A conscious choice has to be made when choosing the right semantic for the API.

  • Nullability of the parameters should be introduced as a signal to allow users to express “absence” of the parameter and corresponding component’s capabilities.
  • Avoid making parameters nullable to utilize null as a “use default in the implementation” signal.
  • Avoid making parameter nullable to signal that the value exists, but is empty, prefer a meaningful empty default value.

DON’T

@Composable
fun IconCard(
    bitmap: ImageBitmap,
    // avoid having null as a signal to gather default
    elevation: Dp? = null
) {
    // instead of implementation based default resolution, provide meaningful default
    val resolvedElevation = elevation ?: DefaultElevation
}

Do:

@Composable
fun IconCard(
    bitmap: ImageBitmap,
    elevation: Dp = 8.dp
) { ... }

Or Do (null is meaningful here):

@Composable
fun IconCard(
    bitmap: ImageBitmap,
    // null description is NOT the same as "" description
    // since when it is null - we don't add any accessibility info.
    contentDescription: String?
) { ... }

Default expressions

Developers should make sure that default expressions on optional parameters are publicly available and meaningful. Best practices:

  • Default expression does not contain private/internal calls. This allows users that wrap/extend components to provide the same default. Alternatively, this default can be used by the users in the if statement: if (condition) default else myUserValue.
  • Default should have meaningful value, it should be clear what the default value is. Avoid using null as a marker to use the default value internally. Refer to null as the “absence” of the value (per “nullable parameter” section). Absence of the value (null) is a valid default in this case.
  • Use ComponentDefaults objects to name-space defaults values if you have a number of them.

DON’T

@Composable
fun IconCard(
    bitmap: ImageBitmap,
    //backgroundColor has meaningful default, but it is inaccessible to users
    backgroundColor: Color = DefaultBackgroundColor,
    // avoid having null as a signal to gather default
    elevation: Dp? = null
) {
    // instead of implementation based default resolution, provide meaningful default
    val resolvedElevation = elevation ?: DefaultElevation
}

// this default expression is private.
// Users unable to access it when wrapping your component.
private val DefaultBackgroundColor = Color.Red
private val DefaultElevation = 8.dp

Do:

@Composable
fun IconCard(
    bitmap: ImageBitmap,
    //all params have meaningful defaults that are accessible
    backgroundColor: Color = IconCardDefaults.BackgroundColor,
    elevation: Dp = IconCardDefaults.Elevation
) { ... }

// defaults namespaced in the ComponentNameDefaults object and public
object IconCardDefaults {
    val BackgroundColor = Color.Red
    val Elevation = 8.dp
}

Note: If your component has a limited number of parameters that have short and predictable defaults (elevation = 0.dp), ComponentDefaults object might be omitted in favor of simple inline constants.

MutableState<T> as a parameter

Parameters of type MutableState<T> are discouraged since it promotes joint ownership over a state between a component and its user. If possible, consider making the component stateless and concede the state change to the caller. If mutation of the parent’s owned property is required in the component, consider creating a ComponentState class with the domain specific meaningful field that is backed by mutableStateOf().

When a component accepts MutableState as a parameter, it gains the ability to change it. This results in the split ownership of the state, and the usage side that owns the state now has no control over how and when it will be changed from within the component’s implementation.

DON’T

@Composable
fun Scroller(
    offset: MutableState<Float>
) {}

Do (stateless version, if possible):

@Composable
fun Scroller(
    offset: Float,
    onOffsetChange: (Float) -> Unit,
) {}

Or do (state-based component version, if stateless not possible):

class ScrollerState {
    val offset: Float by mutableStateOf(0f)
}

@Composable
fun Scroller(
    state: ScrollerState
) {}

State<T> as a parameter

Parameters of type State<T> are discouraged since it unnecessarily narrows the type of objects that can be passed in the function. Given param: State<Float>, there are two better alternatives available, depending on the use case:

  1. param: Float. If the parameter doesn’t change often, or is being read immediately in the component (composition), developers can provide just a plain parameter and recompose the component when it changes.
  2. param: () -> Float. To delay reading the value until a later time via param.invoke(), lambda might be provided as a parameter. This allows the developers of the component to read the value only when/if it is needed and avoid unnecessary work. For example, if the value is only read during drawing operation, only redraw will occur. This leaves the flexibility to the user to provide any expression, including the State<T>’s read:
    1. param = { myState.value } - read the State<T>’s value
    2. param = { justValueWithoutState } - plain value not backed by the State<T>
    3. param = { myObject.offset } - user can have a custom state object where the field (e.g. offset) is backed by the mutableStateOf()

DON’T

fun Badge(position: State<Dp>) {}

// not possible since only State<T> is allowed
Badge(position = scrollState.offset) // DOES NOT COMPILE

Do:

fun Badge(position: () -> Dp) {}

// works ok
Badge(position = { scrollState.offset })

Slot parameters

What are slots

Slot is a @Composable lambda parameter that specifies a certain sub hierarchy of the component. Content slot in a Button might look like this:

@Composable
fun Button(
    onClick: () -> Unit,
    content: @Composable () -> Unit
) {}

// usage
Button(onClick = { /* handle the click */}) {
    Icon(...)
}

This pattern allows the button to have no opinion on the content, while playing the role of drawing the necessary decoration around, handling clicks and showing ripples.

Why slots

It might be tempting to write the button as follows:

DON’T

@Composable
fun Button(
    onClick: () -> Unit,
    text: String? = null,
    icon: ImageBitmap? = null
) {}

Where either text or icon or both are present, leaving the button to arrange the show. While it handles basic use cases or sample usages well, it has some fundamental flexibility flaws:

  • Restricts styling choice: by using only String, Button disallows users to use AnnotatedString or other sources of text information, if required. To provide some styling, Button will have to accept TextStyle parameters as well, plus some other ones. This will bloat the API of the button quickly.
  • Restricts component choice: While Button might want to show a text, String might not be enough. If a user has their own MyTextWithLogging() component, they might want to use it in a button to do some additional logic like logging events and such. This is impossible with the String API unless the user forks the Button.
  • Overloads explosion: If we want some flexibility, for example accepting both ImageBitmap and VectorPainter as icons, we have to provide an overload for that. We can multiply it for every such parameter (text being String or AnnotatedString or CharSequence), resulting in the big number of overloads we have to provide in order to cater the users’ use cases.
  • Restricts component layout capabilities: In the example above, the Button is opinionated about the arrangement between text and icon. If a user has a special icon that they want to put with a custom arrangement (e.g. on a button’s text baseline or with 4dp additional padding) - they won’t be able to do it.

Slot APIs in components are free from these problems, as a user can pass any component with any styling in a slot. Slots come with the price of simple usages being a bit more verbose, but this downside disappears quickly as soon as a real-application usage begins.

Do

@Composable
fun Button(
    onClick: () -> Unit,
    text: @Composable () -> Unit,
    icon: @Composable () -> Unit
) {}

Single “content” slot overloads

For components that are responsible for layouting of multiple slot APIs it accepts, consider providing an overload with a single slot, usually named content. This allows for more flexibility on the usage side when needed as it is possible to change the slot layout logic.

Do

@Composable
fun Button(
    onClick: () -> Unit,
    content: @Composable () -> Unit
) {}

// usage
Button(onClick = { /* handle the click */}) {
    Row {
        Icon(...)
        Text(...)
   }
}

Layout strategy scope for slot APIs

If applicable, consider choosing an appropriate layout strategy for the slot lambda. This is especially important for single content overloads. In the example above, developers of the Button might notice that most common usage patterns include: single text, single icon, icon and text in a row, text then icon in a row. It might make sense to provide RowScope in a content slot, making it easier for the user to use the button

Do

@Composable
fun Button(
    onClick: () -> Unit,
    content: @Composable RowScope.() -> Unit
) {}

// usage
Button(onClick = { /* handle the click */ }) { // this: RowScope
    Icon(...)
    Text(...)
}

ColumnScope or BoxScope are good candidates for other types of layout strategies for components. The author of the component SHOULD always think about what will happen if multiple components are passed in a slot and consider communicating this behaviour to a user via scopes (RowScope in a Button example above).

Lifecycle expectations for slot parameters

Developers should ensure that the lifecycle of the visible and composed slot parameter composables is either the same as the composable that accepts that slot, or is tied to visibility of the slot in the viewport.

@Composable components that are passed in the slot should not be disposed of and composed again on the structural or visual changes in the parent component.

If in need to make structural changes internally that affect slot composables lifecycle, use remember{} and movableContentOf()

DON’T

@Composable
fun PreferenceItem(
    checked: Boolean,
    content: @Composable () -> Unit
) {
    // don't: this logic will dispose and compose again from scratch the content() composable on the `checked` boolean change
    if (checked) {
        Row {
            Text("Checked")
            content()
        }
    } else {
        Column {
            Text("Unchecked")
            content()
        }
    }
}

Do

@Composable
fun PreferenceItem(
    checked: Boolean,
    content: @Composable () -> Unit
) {
    Layout({
        Text("Preference item")
        content()
    }) {
        // custom layout that relayouts the same instance of `content`
        // when `checked` changes
    }
}

Or Do

@Composable
fun PreferenceItem(
    checked: Boolean,
    content: @Composable () -> Unit
) {
    // this call preserves the lifecycle of `content` between row and column
    val movableContent = remember(content) { movableContentOf(content)}
    if (checked) {
        Row {
            Text("Checked")
            movableContent()
        }
    } else {
        Column {
            Text("Unchecked")
            movableContent()
        }
    }
}

It is expected that slots that become absent from the UI or leave the view port will be disposed of and composed again when they become visible:

Do:

@Composable
fun PreferenceRow(
    checkedContent: @Composable () -> Unit,
    checked: Boolean
) {
    // since checkedContent() is only visible in the checked state
    // it is ok for this slot to be disposed when not present
    // and be composed again when present again
    if (checked) {
        Row {
            Text("Checked")
            checkedContent()
        }
    } else {
        Column {
            Text("Unchecked")
        }
    }
}

DSL based slots

Avoid DSL based slots and APIs where possible and prefer simple slot @Composable lambdas. While giving the developers control over what the user might place in the particular slot, DSL API still restricts the choice of component and layout capabilities. Moreover, the DSL introduces the new API overhead for users to learn and for developers to support.

DON’T

@Composable
fun TabRow(
    tabs: TabRowScope.() -> Unit
) {}

interface TabRowScope {
    // can be a string
    fun tab(string: String)
    // Can be a @composable as well
    fun tab(tabContent: @Composable () -> Unit)
}

Instead of DSL, consider relying on plain slots with parameters. This allows the users to operate with tools they already know while not sacrificing any flexibility.

Do instead:

@Composable
fun TabRow(
    tabs: @Composable () -> Unit
) {}

@Composable
fun Tab(...) {}

// usage
TabRow {
    tabsData.forEach { data ->
        Tab(...)
    }
}

DSL for defining content of the component or its children should be perceived as an exception. There are some cases that benefit from the DSL approach, notably when the component wants to lazily show and compose only the subset of children (e.g. LazyRow, LazyColumn).

Allowed, since laziness and flexibility with different data types is needed:

@Composable
fun LazyColumn(
    content: LazyListScope.() -> Unit
) {}

// usage: DSL is fine since it allows Lazycolumn to lazily compose the subset of children
LazyColumn {
    // allow to define different types of children and treat them differently
    // since sticky header can act both like an item and a sticky header
    stickyHeader {
        Text("Header")
    }
    items(...) {
        Text($index)
    }
}

Even in such cases like with LazyColumn it is possible to define the API structure without DSL, so simple version should be considered first

Do. Simpler, easier to learn and use API that still provides laziness of children composition:

@Composable
fun HorizontalPager(
    // pager still lazily composes pages when needed
    // but the api is simpler and easier to use; no need for DSL
    pageContent: @Composable (pageIndex: Int) -> Unit
) {}

Component-related classes and functions

Jetpack Compose framework development MUST follow the rules in this section.

Library development SHOULD follow the section below.

App development MAY follow the rules below.

State

For core design practices with state, visit corresponding section in compose api guidelines.

ComponentDefault object

All component default expressions should either be inline or live in the top level object called ComponentDefaults, where Component is a real component name. Refer to the “Default expressions” section for details.

ComponentColor/ComponentElevation objects

Consider a simple if-else expression in the default statements for a simple branching logic, or a dedicated ComponentColor/ComponentElevation class that clearly defines the inputs that a particular Color/Elevation can be reflected against.

There’s a number of ways to provide and/or allow customisation of a certain single type of parameters (e.g. colors, dp) depending on the state of the component (e.g. enabled/disabled, focused/hovered/pressed).

Do (if color choosing logic is simple)

@Composable
fun Button(
    onClick: () -> Unit,
    enabled: Boolean = true,
    backgroundColor =
        if (enabled) ButtonDefaults.enabledBackgroundColor
        else ButtonDefaults.disabledBackgroundColor,
    elevation =
        if (enabled) ButtonDefaults.enabledElevation
        else ButtonDefaults.disabledElevation,
    content: @Composable RowScope.() -> Unit
) {}

While this works well, those expressions can grow pretty quickly and pollute the API space. That’s why it might be sensible to isolate this to a domain and parameter specific class.

Do (if color conditional logic is more complicated)

class ButtonColors(
    backgroundColor: Color,
    disabledBackgroundColor: Color,
    contentColor: Color,
    disabledContentColor: Color
) {
    fun backgroundColor(enabled: Boolean): Color { ... }

    fun contentColor(enabled: Boolean): Color { ... }
}

object ButtonDefaults {
    // default factory for the class
    // can be @Composable to access the theme composition locals
    fun colors(
        backgroundColor: Color = ...,
        disabledBackgroundColor: Color = ...,
        contentColor: Color = ...,
        disabledContentColor: Color = ...
    ): ButtonColors { ... }
}

@Composable
fun Button(
    onClick: () -> Unit,
    enabled: Boolean = true,
    colors: ButtonColors = ButtonDefaults.colors(),
    content: @Composable RowScope.() -> Unit
) {
    val resolvedBackgroundColor = colors.backgroundColor(enabled)
}

This way, while not introducing the overhead and complexities of the “styles” pattern, we isolate the configuration of a specific part of the component. Additionally, unlike plain default expression, ComponentColors or ComponentElevation classes allow for more granular control, where the user can specify the enabled and disabled colors/elevation separately.

Note: This approach is different from styles that are discouraged in compose “no styles” chapter for rationale. ComponentColor and other such classes target a certain type of functionality of the component, allowing for definition of the color against explicit inputs. The instances of this class must be passed as an explicit parameter for the component.

Note: While ComponentColors and ComponentElevation are the most common patterns, there are other component parameters that can be isolated in the similar fashion.

Documentation for the component

Jetpack Compose framework development SHOULD follow the rules in this section below.

Compose library development MAY follow the rules in the sections below.

App development MAY follow.

Documentation for @Composable components should follow JetBrains’s ktdoc guidelines and syntax. Additionally, documentation must communicate a component's capabilities to developers via multiple channels: description of the component purpose, parameters and expectations about those parameters, usage examples.

Documentation structure and ordering

Every component should have following documentation structure:

  1. One-liner paragraph summarizing the component and what it does.
  2. Paragraphs going more into the detail of components, outlining the capabilities, behavior and might include one or more of:
    • @sample tag providing an example of the usage for this components and its states, default, etc. If you don't have access to @sample functionality, consider inline examples in the ktdoc.
    • @see tags pointing to other related apis.
    • Links to design or other materials to help to use the components to its full potential.
  3. Description for each parameter of the component, starting with @param paramname.
    • Developers might decide to optionally omit the documentation for the trailing @Composable content lambda as it is always implied to be the main content slot for the component.

Documentation example

Do

/**
* Material Design badge box.
*
* A badge represents dynamic information such as a number of pending requests in a navigation bar. Badges can be icon only or contain short text.
*
* ![Badge image](https://developer.android.com/images/reference/androidx/compose/material3/badge.png)
*
* A common use case is to display a badge with navigation bar items.
* For more information, see [Navigation Bar](https://m3.material.io/components/navigation-bar/overview)
*
* A simple icon with badge example looks like:
* @sample androidx.compose.material3.samples.NavigationBarItemWithBadge
*
* @param badge the badge to be displayed - typically a [Badge]
* @param modifier the [Modifier] to be applied to this BadgedBox
* @param content the anchor to which this badge will be positioned
*/
@ExperimentalMaterial3Api
@Composable
fun BadgedBox(
    badge: @Composable BoxScope.() -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit
)

Accessibility of the component

Consider using foundation building blocks like Modifier.clickable or Image for better accessibility. Those building blocks will provide good defaults when possible, or will explicitly ask for needed information. Accessibility needs to be manually handled when using ui-level blocks, such as Layout or Modifier.pointerInput. This section contains best practices regarding accessible API design and accessibility implementation tuning.

Semantics merging

Jetpack Compose uses semantics merging for accessibility purposes. This way, Button with the content slot doesn’t have to set the text for accessibility service to announce. Instead, the content’s semantics (Icon’s contentDescription or Text’s text) will be merged into the button. Refer to the official documentation for more info.

To manually create a node that will merge all of its children, you can set a Modifier.semantics(mergeDescendants = true) modifier to your component. This will force all non-merging children to collect and pass the data to your component, so it will be treated as a single entity. Some foundation-layer modifiers merge descendants by default (example: Modifier.clickable or Modifier.toggleable).

Accessibility related parameters

For especially common accessibility needs, developers might want to accept some accessibility-related parameters to let users help to provide better accessibility. This is especially true for leaf components like Image or Icon. Image has a required parameter contentDescription to signal to the user the need to pass the necessary description for an image. When developing components, developers need to make a conscious decision on what to build in in the implementation vs what to ask from the user via parameters.

Note that if you follow the normal best practice of providing an ordinary Modifier parameter and put it on your root layout element, this on its own provides a large amount of implicit accessibility customizability. Because the user of your component can provide their own Modifier.semantics which will apply to your component. In addition, this also provides a way for developers to override a portion of your component’s default semantics: if there are two SemanticsProperties with identical keys on one modifier chain, Compose resolves the conflict by having the first one win and the later ones ignored.

Therefore, you don’t need to add a parameter for every possible semantics your component might need. You should reserve them for especially common cases where it would be inconvenient to write out the semantics block every time, or use cases where for some reason the Modifier mechanism doesn’t work (for example, you need to add semantics to an inner child of your component).

Accessibility tuning

While basic accessibility capabilities will be granted by using foundation layer building blocks, there’s a potential for developers to make the component more accessible.

There are specific semantics expected for individual categories of components: simple components typically require 1-3 semantics, whereas more complex components like text fields, scroll containers or time/date pickers require a very rich set of semantics to function correctly with screenreaders. When developing a new custom component, first consider which of the existing standard Compose components it’s most similar to, and imitating the semantics provided by that component’s implementation, and the exact foundation building blocks it uses. Go from there to fine-tune and add more semantical actions and/or properties when needed.

Evolution of the Component APIs

Jetpack Compose framework development MUST follow the rules in this section below.

Compose library development MUST follow the rules in the sections below.

App development MAY follow.

Refer to the kotlin backwards compatibility guidelines for additional information.

Since every compose is a function, the following rules apply to the component API changes:

  • Parameters of the functions MUST NOT be removed.
  • Newly added parameter to existing functions MUST have default expressions.
  • New parameters MAY be added as a last parameter, or second to last in cases of trailing lambdas.
    • The developer might decide to put the new parameter closer to other parameters that are semantically close to a new one. Keep in mind that this might break source compatibility if the user uses the component without named parameters.

The workflow to add a new parameter to a component:

  1. Create a new overload with the new parameter containing the default.
  2. Deprecate the existing function with DeprecationLevel.Hidden for binary compatibility.
  3. Make the deprecated version to call your new one.

Do:

// existing API we want to extend
@Deprecated(
    "Maintained for compatibility purposes. Use another overload",
    level = DeprecationLevel.HIDDEN
)
@Composable
fun Badge(color: Color) {}

// new overload has to be created
@Composable
fun Badge(
    color: Color,
    // default should be provided
    secondaryColor: Color = Color.Blue
) {}