@Composable
components in Jetpack ComposeSet 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.
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.
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 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.
@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:
When creating a new component:
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, ) { ... }
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(...) } }
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.
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() }
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.
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, ... )
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
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.
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 ) }
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”.
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.
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
parameterEvery component that emits UI should have a modifier parameter. Make sure that modifier parameter:
Modifier
.Modifier
.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)) { ... } }
The order of parameters in a component must be as follows:
modifier: Modifier = Modifier
.@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:
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.modifier
parameter, they do not require the user to make an immediate choice and allow one-by-one override using named parameters.@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 )
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.
null
as a “use default in the implementation” signal.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? ) { ... }
Developers should make sure that default expressions on optional parameters are publicly available and meaningful. Best practices:
if (condition) default else myUserValue
.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.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.
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 ) {}
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:
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.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:param = { myState.value }
- read the State<T>
’s valueparam = { justValueWithoutState }
- plain value not backed by the State<T>
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 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.
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:
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.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.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.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 ) {}
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(...) } }
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).
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") } } }
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 ) {}
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.
For core design practices with state, visit corresponding section in compose api guidelines.
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.
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.
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.
Every component should have following documentation structure:
@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.@param paramname
.@Composable
content
lambda as it is always implied to be the main content slot for the component.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 )
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.
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
).
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).
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.
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.
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.
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 such as SemanticsMatcher to affect the internal state.
Android specific components should ideally support both Compose Preview Screenshot Testing and Robolectric (RNG) to enable effective screenshot testing.
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:
The workflow to add a new parameter to a component:
DeprecationLevel.Hidden
for binary compatibility.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 ) {}