blob: 76c2a5c97f2ad31a7c715b4f7bd0ebec352c66c9 [file] [log] [blame] [view]
# 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](https://www.ietf.org/rfc/rfc2119.txt) 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](#note-on-vocabulary-in-this-doc)
- [Before you create a component](#before-you-create-a-component)
- [Component’s purpose](#components-purpose)
- [Component layering](#component-layering)
- [Do you need a component?](#do-you-need-a-component)
- [Component or Modifier](#component-or-modifier)
- [Name of a Component](#name-of-a-component)
- [BasicComponent vs Component](#basiccomponent-vs-component)
- [Design, Usecase or Company/Project specific prefixes](#design-usecase-or-companyproject-specific-prefixes)
- [Component dependencies](#component-dependencies)
- [Prefer multiple components over style classes](#prefer-multiple-components-over-style-classes)
- [Explicit vs implicit dependencies](#explicit-vs-implicit-dependencies)
- [Component parameters](#component-parameters)
- [Parameters vs. Modifier on the component](#parameters-vs-modifier-on-the-component)
- [`modifier` parameter](#modifier-parameter)
- [Parameters order](#parameters-order)
- [Nullable parameter](#nullable-parameter)
- [Default expressions](#default-expressions)
- [MutableState\<T\> as a parameter](#mutablestatet-as-a-parameter)
- [State\<T\> as a parameter](#statet-as-a-parameter)
- [Slot parameters](#slot-parameters)
- [What are slots](#what-are-slots)
- [Why slots](#why-slots)
- [Single “content” slot overloads](#single-content-slot-overloads)
- [Layout strategy scope for slot APIs](#layout-strategy-scope-for-slot-apis)
- [Lifecycle expectations for slot parameters](#lifecycle-expectations-for-slot-parameters)
- [DSL based slots](#dsl-based-slots)
- [Component-related classes and functions](#component-related-classes-and-functions)
- [State](#state)
- [ComponentDefault object](#componentdefault-object)
- [ComponentColor/ComponentElevation objects](#componentcolorcomponentelevation-objects)
- [Documentation for the component](#documentation-for-the-component)
- [Documentation structure and ordering](#documentation-structure-and-ordering)
- [Documentation example](#documentation-example)
- [Accessibility of the component](#accessibility-of-the-component)
- [Semantics merging](#semantics-merging)
- [Accessibility related parameters](#accessibility-related-parameters)
- [Accessibility tuning](#accessibility-tuning)
- [Tooling support](#tooling-support)
- [Evolution of the Component APIs](#evolution-of-the-component-apis)
## 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](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-api-guidelines.md#naming-unit-composable-functions-as-entities) 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](#componentcolorcomponentelevation-objects)" 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](https://medium.com/androiddevelopers/pushing-the-right-buttons-in-jetpack-compose-124cb4b17197) 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. breaking 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](#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](#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](https://developer.android.com/jetpack/compose/phases#state-reads). 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](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-api-guidelines.md#compose-api-design-patterns).
### 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](#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](#prefer-multiple-components-over-style-classes)" 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](https://kotlinlang.org/docs/kotlin-doc.html#kdoc-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](https://developer.android.com/jetpack/compose/semantics#merged-vs-unmerged) 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.
## Tooling support
**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.
Consider component behaviour in app developer tooling including Android Studio
Previews and test infrastructure. Components are expected to behave correctly in
those environments to make the developer experience productive.
### Compose Preview tooling
Components are expected to display initial state when used in non-interactive
preview mode.
Components should avoid patterns that delay the initial render to a subsequent
frame. Avoid using LaunchedEffects or asynchronous logic for initial component
state set up.
If required use `LocalInspectionMode.current` to detect when running as a
preview, and do the minimal change to ensure Previews are functional. Avoid
replacing a complex component with some placeholder image in Previews. Ensure
your component works correctly with various parameters provided via the preview
tooling.
In interactive mode, Previews should allow direct use of the component with the
same interactive experience as when running in an application.
### Screenshot testing
Components should support screenshot testing.
Prefer stateless components where state is passed as a parameter to make sure
the component is screenshot-testable in various states. Alternatively, support
use of
[Compose testing APIs](https://developer.android.com/develop/ui/compose/testing/apis)
such as SemanticsMatcher to affect the internal state.
Android specific components should ideally support both
[Compose Preview Screenshot Testing](https://developer.android.com/studio/preview/compose-screenshot-testing)
and Robolectric
([RNG](https://github.com/robolectric/robolectric/releases/tag/robolectric-4.10))
to enable effective screenshot testing.
## 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](https://kotlinlang.org/docs/jvm-api-guidelines-backward-compatibility.html) 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
) {}
```