blob: 668000272d59b766f39ab95d882821784904fa8f [file] [log] [blame] [view]
Adam Powellc814a3d2021-03-10 11:57:20 -08001# API Guidelines for Jetpack Compose
2
3## Last updated: March 10, 2021
4
5# Who this document is for
6
7The Compose API guidelines outline the patterns, best practices and prescriptive style guidelines for writing idiomatic Jetpack Compose APIs. As Jetpack Compose code is built in layers, everyone writing code that uses Jetpack Compose is building their own API to consume themselves.
8
9This document assumes a familiarity with Jetpack Compose's runtime APIs including `@Composable`, `remember {}` and `CompositionLocal`.
10
11The 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.
12
13## Jetpack Compose framework development
14
15Contributions to the `androidx.compose` libraries and tools generally follow these guidelines to a strict degree in order to promote consistency, setting expectations and examples for consumer code at all layers.
16
17## Library development based on Jetpack Compose
18
19It 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.
20
21## App development based on Jetpack Compose
22
23App development is often subject to strong organizational priorities and norms as well as 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.
24
25# Kotlin style
26
27## Baseline style guidelines
28
29**Jetpack Compose framework development** MUST follow the Kotlin Coding Conventions outlined at https://kotlinlang.org/docs/reference/coding-conventions.html as a baseline with the additional adjustments described below.
30
31**Jetpack Compose Library and app development** SHOULD also follow this same guideline.
32
33### Why
34
35The Kotlin Coding Conventions establish a standard of consistency for the Kotlin ecosystem at large. The additional style guidelines that follow in this document for Jetpack Compose account for Jetpack Compose's language-level extensions, mental models, and intended data flows, establishing consistent conventions and expectations around Compose-specific patterns.
36
37## Singletons, constants, sealed class and enum class values
38
39**Jetpack Compose framework development** MUST name deeply immutable constants following the permitted object declaration convention of `PascalCase` as documented [here](https://kotlinlang.org/docs/reference/coding-conventions.html#property-names) as a replacement for any usage of `CAPITALS_AND_UNDERSCORES`. Enum class values MUST also be named using `PascalCase` as documented in the same section.
40
41**Library development** SHOULD follow this same convention when targeting or extending Jetpack Compose.
42
43**App Development** MAY follow this convention.
44
45### Why
46
47Jetpack Compose discourages the use and creation of singletons or companion object state that cannot be treated as _stable_ over time and across threads, reducing the usefulness of a distinction between singleton objects and other forms of constants. This forms a consistent expectation of API shape for consuming code whether the implementation detail is a top-level `val`, a `companion object`, an `enum class`, or a `sealed class` with nested `object` subclasses. `myFunction(Foo)` and `myFunction(Foo.Bar)` carry the same meaning and intent for calling code regardless of specific implementation details.
48
49Library and app code with a strong existing investment in `CAPITALS_AND_UNDERSCORES` in their codebase MAY opt for local consistency with that pattern instead.
50
51### Do
52
53```kotlin
54const val DefaultKeyName = "__defaultKey"
55
56val StructurallyEqual: ComparisonPolicy = StructurallyEqualsImpl(...)
57
58object ReferenceEqual : ComparisonPolicy {
59 // ...
60}
61
62sealed class LoadResult<T> {
63 object Loading : LoadResult<Nothing>()
64 class Done(val result: T) : LoadResult<T>()
65 class Error(val cause: Throwable) : LoadResult<Nothing>()
66}
67
68enum class Status {
69 Idle,
70 Busy
71}
72```
73
74### Don't
75
76```kotlin
77const val DEFAULT_KEY_NAME = "__defaultKey"
78
79val STRUCTURALLY_EQUAL: ComparisonPolicy = StructurallyEqualsImpl(...)
80
81object ReferenceEqual : ComparisonPolicy {
82 // ...
83}
84
85sealed class LoadResult<T> {
86 object Loading : LoadResult<Nothing>()
87 class Done(val result: T) : LoadResult<T>()
88 class Error(val cause: Throwable) : LoadResult<Nothing>()
89}
90
91enum class Status {
92 IDLE,
93 BUSY
94}
95```
96
97# Compose baseline
98
99The Compose compiler plugin and runtime establish new language facilities for Kotlin and the means to interact with them. This layer adds a declarative programming model for constructing and managing mutable tree data structures over time. Compose UI is an example of one type of tree that the Compose runtime can manage, but it is not limited to that use.
100
101This section outlines guidelines for `@Composable` functions and APIs that build on the Compose runtime capabilities. These guidelines apply to all Compose runtime-based APIs, regardless of the managed tree type.
102
103## Naming Unit @Composable functions as entities
104
105**Jetpack Compose framework development and Library development** MUST name any function that returns `Unit` and bears the `@Composable` annotation using `PascalCase`, and the name MUST be that of a noun, not a verb or verb phrase, nor a nouned preposition, adjective or adverb. Nouns MAY be prefixed by descriptive adjectives. This guideline applies whether the function emits UI elements or not.
106
107**App development** SHOULD follow this same convention.
108
109### Why
110
111Composable functions that return `Unit` are considered _declarative entities_ that can be either _present_ or _absent_ in a composition and therefore follow the naming rules for classes. A composable's presence or absence resulting from the evaluation of its caller's control flow establishes both persistent identity across recompositions and a lifecycle for that persistent identity. This naming convention promotes and reinforces this declarative mental model.
112
113### Do
114
115```kotlin
116// This function is a descriptive PascalCased noun as a visual UI element
117@Composable
118fun FancyButton(text: String, onClick: () -> Unit) {
119```
120
121### Do
122
123```kotlin
124// This function is a descriptive PascalCased noun as a non-visual element
125// with presence in the composition
126@Composable
127fun BackButtonHandler(onBackPressed: () -> Unit) {
128```
129
130### Don't
131
132```kotlin
133// This function is a noun but is not PascalCased!
134@Composable
135fun fancyButton(text: String, onClick: () -> Unit) {
136```
137
138### Don't
139
140```kotlin
141// This function is PascalCased but is not a noun!
142@Composable
143fun RenderFancyButton(text: String, onClick: () -> Unit) {
144```
145
146### Don't
147
148```kotlin
149// This function is neither PascalCased nor a noun!
150@Composable
151fun drawProfileImage(image: ImageAsset) {
152```
153
154## Naming @Composable functions that return values
155
156**Jetpack Compose framework development and Library development** MUST follow the standard [Kotlin Coding Conventions for the naming of functions](https://kotlinlang.org/docs/reference/coding-conventions.html#function-names) for any function annotated `@Composable` that returns a value other than `Unit`.
157
158**Jetpack Compose framework development and Library development** MUST NOT use the factory function exemption in the [Kotlin Coding Conventions for the naming of functions](https://kotlinlang.org/docs/reference/coding-conventions.html#function-names) for naming any function annotated `@Composable` as a PascalCase type name matching the function's abstract return type.
159
160### Why
161
162While useful and accepted outside of `@Composable` functions, this factory function convention has drawbacks that set inappropriate expectations for callers when used with `@Composable` functions.
163
164Primary motivations for marking a factory function as `@Composable` include using composition to establish a managed lifecycle for the object or using `CompositionLocal`s as inputs to the object's construction. The former implies the use of Compose's `remember {}` API to cache and maintain the object instance across recompositions, which can break caller expectations around a factory operation that reads like a constructor call. (See the next section.) The latter motivation implies unseen inputs that should be expressed in the factory function name.
165
166Additionally, the mental model of `Unit`-returning `@Composable` functions as declarative entities should not be confused with a, "virtual DOM" mental model. Returning values from `@Composable` functions named as `PascalCase` nouns promotes this confusion, and may promote an undesirable style of returning a stateful control surface for a present UI entity that would be better expressed and more useful as a hoisted state object.
167
168More information about state hoisting patterns can be found in the design patterns section of this document.
169
170### Do
171
172```kotlin
173// Returns a style based on the current CompositionLocal settings
174// This function qualifies where its value comes from
175@Composable
176fun defaultStyle(): Style {
177```
178
179### Don't
180
181```kotlin
182// Returns a style based on the current CompositionLocal settings
183// This function looks like it's constructing a context-free object!
184@Composable
185fun Style(): Style {
186```
187
188## Naming @Composable functions that remember {} the objects they return
189
190**Jetpack Compose framework development and Library development** MUST prefix any `@Composable` factory function that internally `remember {}`s and returns a mutable object with the prefix `remember`.
191
192**App development** SHOULD follow this same convention.
193
194### Why
195
196An object that can change over time and persists across recompositions carries observable side effects that should be clearly communicated to a caller. This also signals that a caller does not need to duplicate a `remember {}` of the object at the call site to attain this persistence.
197
198### Do
199
200```kotlin
201// Returns a CoroutineScope that will be cancelled when this call
202// leaves the composition
203// This function is prefixed with remember to describe its behavior
204@Composable
205fun rememberCoroutineScope(): CoroutineScope {
206```
207
208### Don't
209
210```kotlin
211// Returns a CoroutineScope that will be cancelled when this call leaves
212// the composition
213// This function's name does not suggest automatic cancellation behavior!
214@Composable
215fun createCoroutineScope(): CoroutineScope {
216```
217
218Note that returning an object is not sufficient to consider a function to be a factory function; it must be the function's primary purpose. Consider a `@Composable` function such as `Flow<T>.collectAsState()`; this function's primary purpose is to establish a subscription to a `Flow`; that it `remember {}`s its returned `State<T>` object is incidental.
219
220## Naming CompositionLocals
221
222A `CompositionLocal` is a key into a composition-scoped key-value table. `CompositionLocal`s may be used to provide global-like values to a specific subtree of composition.
223
224**Jetpack Compose framework development and Library development** MUST NOT name `CompositionLocal` keys using "CompositionLocal" or "Local" as a noun suffix. `CompositionLocal` keys should bear a descriptive name based on their value.
225
226**Jetpack Compose framework development and Library development** MAY use "Local" as a prefix for a `CompositionLocal` key name if no other, more descriptive name is suitable.
227
228### Do
229
230```kotlin
231// "Local" is used here as an adjective, "Theme" is the noun.
232val LocalTheme = staticCompositionLocalOf<Theme>()
233```
234
235### Don't
236
237```kotlin
238// "Local" is used here as a noun!
239val ThemeLocal = staticCompositionLocalOf<Theme>()
240```
241
242## Stable types
243
244The Compose runtime exposes two annotations that may be used to mark a type or function as _stable_ - safe for optimization by the Compose compiler plugin such that the Compose runtime may skip calls to functions that accept only safe types because their results cannot change unless their inputs change.
245
246The Compose compiler plugin may infer these properties of a type automatically, but interfaces and other types for which stability can not be inferred, only promised, may be explicitly annotated. Collectively these types are called, "stable types."
247
248**`@Immutable`** indicates a type where the value of any properties will **never** change after the object is constructed, and all methods are **referentially transparent**. All Kotlin types that may be used in a `const` expression (primitive types and Strings) are considered `@Immutable`.
249
250**`@Stable`** when applied to a type indicates a type that is **mutable**, but the Compose runtime will be notified if and when any public properties or method behavior would yield different results from a previous invocation. (In practice this notification is backed by the `Snapshot` system via `@Stable` `MutableState` objects returned by `mutableStateOf()`.) Such a type may only back its properties using other `@Stable` or `@Immutable` types.
251
252**Jetpack Compose framework development, Library development and App development** MUST ensure in custom implementations of `.equals()` for `@Stable` types that for any two references `a` and `b` of `@Stable` type `T`, `a.equals(b)` MUST **always** return the same value. This implies that any **future** changes to `a` must also be reflected in `b` and vice versa.
253
254This constraint is always met implicitly if `a === b`; the default reference equality implementation of `.equals()` for objects is always a correct implementation of this contract.
255
256**Jetpack Compose framework development and Library development** SHOULD correctly annotate `@Stable` and `@Immutable` types that they expose as part of their public API.
257
258**Jetpack Compose framework development and Library development** MUST NOT remove the `@Stable` or `@Immutable` annotation from a type if it was declared with one of these annotations in a previous stable release.
259
260**Jetpack Compose framework development and Library development** MUST NOT add the `@Stable` or `@Immutable` annotation to an existing non-final type that was available in a previous stable release without this annotation.
261
262### Why?
263
264`@Stable` and `@Immutable` are behavioral contracts that impact the binary compatibility of code generated by the Compose compiler plugin. Libraries should not declare more restrictive contracts for preexisting non-final types that existing implementations in the wild may not correctly implement, and similarly they may not declare that a library type no longer obeys a previously declared contract that existing code may depend upon.
265
266Implementing the stable contract incorrectly for a type annotated as `@Stable` or `@Immutable` will result in incorrect behavior for `@Composable` functions that accept that type as a parameter or receiver.
267
268## Emit XOR return a value
269
270`@Composable` functions should either emit content into the composition or return a value, but not both. If a composable should offer additional control surfaces to its caller, those control surfaces or callbacks should be provided as parameters to the composable function by the caller.
271
272**Jetpack Compose framework development and Library development** MUST NOT expose any single `@Composable` function that both emits tree nodes and returns a value.
273
274### Why
275
276Emit operations must occur in the order the content is to appear in the composition. Using return values to communicate with the caller restricts the shape of calling code and prevents interactions with other declarative calls that come before it.
277
278### Do
279
280```kotlin
281// Emits a text input field element that will call into the inputState
282// interface object to request changes
283@Composable
284fun InputField(inputState: InputState) {
285// ...
286
287// Communicating with the input field is not order-dependent
288val inputState = remember { InputState() }
289
290Button("Clear input", onClick = { inputState.clear() })
291
292InputField(inputState)
293```
294
295### Don't
296
297```kotlin
298// Emits a text input field element and returns an input value holder
299@Composable
300fun InputField(): UserInputState {
301// ...
302
303// Communicating with the InputField is made difficult
304Button("Clear input", onClick = { TODO("???") })
305val inputState = InputField()
306```
307
308Communicating with a composable by passing parameters forward affords aggregation of several such parameters into types used as parameters to their callers:
309
310```kotlin
311interface DetailCardState {
312 val actionRailState: ActionRailState
313 // ...
314}
315
316@Composable
317fun DetailCard(state: DetailCardState) {
318 Surface {
319 // ...
320 ActionRail(state.actionRailState)
321 }
322}
323
324@Composable
325fun ActionRail(state: ActionRailState) {
326 // ...
327}
328```
329
330For more information on this pattern, see the sections on [hoisted state types](#hoisted-state-types) in the Compose API design patterns section below.
331
332# Compose UI API structure
333
334Compose UI is a UI toolkit built on the Compose runtime. This section outlines guidelines for APIs that use and extend the Compose UI toolkit.
335
336## Compose UI elements
337
338A `@Composable` function that emits exactly one Compose UI tree node is called an _element_.
339
340Example:
341
342```kotlin
343@Composable
344fun SimpleLabel(
345 text: String,
346 modifier: Modifier = Modifier
347) {
348```
349
350**Jetpack Compose framework development and Library development** MUST follow all guidelines in this section.
351
352**Jetpack Compose app development** SHOULD follow all guidelines in this section.
353
354### Elements return Unit
355
356Elements MUST emit their root UI node either directly by calling emit() or by calling another Compose UI element function. They MUST NOT return a value. All behavior of the element not available from the state of the composition MUST be provided by parameters passed to the element function.
357
358#### Why?
359
360Elements are declarative entities in a Compose UI composition. Their presence or absence in the composition determines whether they appear in the resulting UI. Returning a value is not necessary; any means of controlling the emitted element should be provided as a parameter to the element function, not returned by calling the element function. See the, "hoisted state" section in the Compose API design patterns section of this document for more information.
361
362#### Do
363
364```kotlin
365@Composable
366fun FancyButton(
367 text: String,
368 onClick: () -> Unit,
369 modifier: Modifier = Modifier
370) {
371```
372
373#### Don't
374
375```kotlin
376interface ButtonState {
377 val clicks: Flow<ClickEvent>
378 val measuredSize: Size
379}
380
381@Composable
382fun FancyButton(
383 text: String,
384 modifier: Modifier = Modifier
385): ButtonState {
386```
387
388### Elements accept and respect a Modifier parameter
389
Louis Pullen-Freilichcd865502022-08-03 15:12:49 +0000390Element functions MUST accept a parameter of type `Modifier`. This parameter MUST be named "`modifier`" and MUST appear as the first optional parameter in the element function's parameter list. Element functions MUST NOT accept multiple `Modifier` parameters.
Adam Powellc814a3d2021-03-10 11:57:20 -0800391
392If the element function's content has a natural minimum size - that is, if it would ever measure with a non-zero size given constraints of minWidth and minHeight of zero - the default value of the `modifier` parameter MUST be `Modifier` - the `Modifier` type's `companion object` that represents the empty `Modifier`. Element functions without a measurable content size (e.g. Canvas, which draws arbitrary user content in the size available) MAY require the `modifier` parameter and omit the default value.
393
394Element functions MUST provide their modifier parameter to the Compose UI node they emit by passing it to the root element function they call. If the element function directly emits a Compose UI layout node, the modifier MUST be provided to the node.
395
396Element functions MAY concatenate additional modifiers to the **end** of the received `modifier` parameter before passing the concatenated modifier chain to the Compose UI node they emit.
397
398Element functions MUST NOT concatenate additional modifiers to the **beginning** of the received modifier parameter before passing the concatenated modifier chain to the Compose UI node they emit.
399
400#### Why?
401
402Modifiers are the standard means of adding external behavior to an element in Compose UI and allow common behavior to be factored out of individual or base element API surfaces. This allows element APIs to be smaller and more focused, as modifiers are used to decorate those elements with standard behavior.
403
404An element function that does not accept a modifier in this standard way does not permit this decoration and motivates consuming code to wrap a call to the element function in an additional Compose UI layout such that the desired modifier can be applied to the wrapper layout instead. This does not prevent the developer behavior of modifying the element, and forces them to write more inefficient UI code with a deeper tree structure to achieve their desired result.
405
406Modifiers occupy the first optional parameter slot to set a consistent expectation for developers that they can always provide a modifier as the final positional parameter to an element call for any given element's common case.
407
408See the Compose UI modifiers section below for more details.
409
410#### Do
411
412```kotlin
413@Composable
414fun FancyButton(
415 text: String,
416 onClick: () -> Unit,
417 modifier: Modifier = Modifier
418) = Text(
419 text = text,
420 modifier = modifier.surface(elevation = 4.dp)
421 .clickable(onClick)
422 .padding(horizontal = 32.dp, vertical = 16.dp)
423)
424```
425
426## Compose UI layouts
427
428A Compose UI element that accepts one or more `@Composable` function parameters is called a _layout_.
429
430Example:
431
432```kotlin
433@Composable
434fun SimpleRow(
435 modifier: Modifier = Modifier,
436 content: @Composable () -> Unit
437) {
438```
439
440**Jetpack Compose framework development and Library development** MUST follow all guidelines in this section.
441
442**Jetpack Compose app development** SHOULD follow all guidelines in this section.
443
444Layout functions SHOULD use the name "`content`" for a `@Composable` function parameter if they accept only one `@Composable` function parameter.
445
446Layout functions SHOULD use the name "`content`" for their primary or most common `@Composable` function parameter if they accept more than one `@Composable` function parameter.
447
448Layout functions SHOULD place their primary or most common `@Composable` function parameter in the last parameter position to permit the use of Kotlin's trailing lambda syntax for that parameter.
449
450## Compose UI modifiers
451
452A `Modifier` is an immutable, ordered collection of objects that implement the `Modifier.Element` interface. Modifiers are universal decorators for Compose UI elements that may be used to implement and add cross-cutting behavior to elements in an opaque and encapsulated manner. Examples of modifiers include altering element sizing and padding, drawing content beneath or overlapping the element, or listening to touch events within the UI element's bounding box.
453
454**Jetpack Compose framework development and Library development** MUST follow all guidelines in this section.
455
456### Modifier factory functions
457
458Modifier chains are constructed using a fluent builder syntax expressed as Kotlin extension functions that act as factories.
459
460Example:
461
462```kotlin
463Modifier.preferredSize(50.dp)
464 .backgroundColor(Color.Blue)
465 .padding(10.dp)
466```
467
468Modifier APIs MUST NOT expose their Modifier.Element interface implementation types.
469
470Modifier APIs MUST be exposed as factory functions following this style:
471
472```kotlin
473fun Modifier.myModifier(
474 param1: ...,
475 paramN: ...
476): Modifier = then(MyModifierImpl(param1, ... paramN))
477```
478
479### Composed modifiers
480
481Modifiers that must take part in composition (for example, to read `CompositionLocal` values, maintain element-specific instance state or manage object lifetimes) can use the `Modifier.composed {}` API to create a modifier that is a modifier instance factory:
482
483```kotlin
484fun Modifier.myModifier(): Modifier = composed {
485 val color = LocalTheme.current.specialColor
486 backgroundColor(color)
487}
488```
489
490Composed modifiers are composed at each point of application to an element; the same composed modifier may be provided to multiple elements and each will have its own composition state:
491
492```kotlin
493fun Modifier.modifierWithState(): Modifier = composed {
494 val elementSpecificState = remember { MyModifierState() }
495 MyModifier(elementSpecificState)
496}
497
498// ...
499val myModifier = someModifiers.modifierWithState()
500
501Text("Hello", modifier = myModifier)
502Text("World", modifier = myModifier)
503```
504
505As a result, **Jetpack Compose framework development and Library development** SHOULD use `Modifier.composed {}` to implement composition-aware modifiers, and SHOULD NOT declare modifier extension factory functions as `@Composable` functions themselves.
506
507#### Why
508
509Composed modifiers may be created outside of composition, shared across elements, and declared as top-level constants, making them more flexible than modifiers that can only be created via a `@Composable` function call, and easier to avoid accidentally sharing state across elements.
510
511### Layout-scoped modifiers
512
513Android's View system has the concept of LayoutParams - a type of object stored opaquely with a ViewGroup's child view that provides layout instructions specific to the ViewGroup that will measure and position it.
514
515Compose UI modifiers afford a related pattern using `ParentDataModifier` and receiver scope objects for layout content functions:
516
517#### Example
518
519```kotlin
520@Stable
521interface WeightScope {
522 fun Modifier.weight(weight: Float): Modifier
523}
524
525@Composable
526fun WeightedRow(
527 modifier: Modifier = Modifier,
528 content: @Composable WeightScope.() -> Unit
529) {
530// ...
531
532// Usage:
533WeightedRow {
534 Text("Hello", Modifier.weight(1f))
535 Text("World", Modifier.weight(2f))
536}
537```
538
539**Jetpack Compose framework development and library development** SHOULD use scoped modifier factory functions to provide parent data modifiers specific to a parent layout composable.
540
541# Compose API design patterns
542
543This section outlines patterns for addressing common use cases when designing a Jetpack Compose API.
544
545## Prefer stateless and controlled @Composable functions
546
547In this context, "stateless" refers to `@Composable` functions that retain no state of their own, but instead accept external state parameters that are owned and provided by the caller. "Controlled" refers to the idea that the caller has full control over the state provided to the composable.
548
549### Do
550
551```kotlin
552@Composable
553fun Checkbox(
554 isChecked: Boolean,
555 onToggle: () -> Unit
556) {
557// ...
558
559// Usage: (caller mutates optIn and owns the source of truth)
560Checkbox(
561 myState.optIn,
562 onToggle = { myState.optIn = !myState.optIn }
563)
564```
565
566### Don't
567
568```kotlin
569@Composable
570fun Checkbox(
571 initialValue: Boolean,
572 onChecked: (Boolean) -> Unit
573) {
574 var checkedState by remember { mutableStateOf(initialValue) }
575// ...
576
577// Usage: (Checkbox owns the checked state, caller notified of changes)
578// Caller cannot easily implement a validation policy.
579Checkbox(false, onToggled = { callerCheckedState = it })
580```
581
582## Separate state and events
583
584Compose's `mutableStateOf()` value holders are observable through the `Snapshot` system and can notify observers of changes. This is the primary mechanism for requesting recomposition, relayout, or redraw of a Compose UI. Working effectively with observable state requires acknowledging the distinction between _state_ and _events_.
585
586An observable _event_ happens at a point in time and is discarded. All registered observers at the time the event occurred are notified. All individual events in a stream are assumed to be relevant and may build on one another; repeated equal events have meaning and therefore a registered observer must observe all events without skipping.
587
588Observable _state_ raises change _events_ when the state changes from one value to a new, unequal value. State change events are _conflated;_ only the most recent state matters. Observers of state changes must therefore be _idempotent;_ given the same state value the observer should produce the same result. It is valid for a state observer to both skip intermediate states as well as run multiple times for the same state and the result should be the same.
589
590Compose operates on _state_ as input, not _events_. Composable functions are _state observers_ where both the function parameters and any `mutableStateOf()` value holders that are read during execution are inputs.
591
592## Hoisted state types
593
594A pattern of stateless parameters and multiple event callback parameters will eventually reach a point of scale where it becomes unwieldy. As a composable function's parameter list grows it may become appropriate to factor a collection of state and callbacks into an interface, allowing a caller to provide a cohesive policy object as a unit.
595
596### Before
597
598```kotlin
599@Composable
600fun VerticalScroller(
601 scrollPosition: Int,
602 scrollRange: Int,
603 onScrollPositionChange: (Int) -> Unit,
604 onScrollRangeChange: (Int) -> Unit
605) {
606```
607
608### After
609
610```kotlin
611@Stable
612interface VerticalScrollerState {
613 var scrollPosition: Int
614 var scrollRange: Int
615}
616
617@Composable
618fun VerticalScroller(
619 verticalScrollerState: VerticalScrollerState
620) {
621```
622
623In the example above, an implementation of `VerticalScrollerState` is able to use custom get/set behaviors of the related `var` properties to apply policy or delegate storage of the state itself elsewhere.
624
625**Jetpack Compose framework and Library development** SHOULD declare hoisted state types for collecting and grouping interrelated policy. The VerticalScrollerState example above illustrates such a dependency between the scrollPosition and scrollRange properties; to maintain internal consistency such a state object should clamp scrollPosition into the valid range during set attempts. (Or otherwise report an error.) These properties should be grouped as handling their consistency involves handling all of them together.
626
627**Jetpack Compose framework and Library development** SHOULD declare hoisted state types as `@Stable` and correctly implement the `@Stable` contract.
628
629**Jetpack Compose framework and Library development** SHOULD name hoisted state types that are specific to a given composable function as the composable function's name suffixed by, "`State`".
630
631## Default policies through hoisted state objects
632
633Custom implementations or even external ownership of these policy objects are often not required. By using Kotlin's default arguments, Compose's `remember {}` API, and the Kotlin "extension constructor" pattern, an API can provide a default state handling policy for simple usage while permitting more sophisticated usage when desired.
634
635### Example:
636
637```kotlin
638fun VerticalScrollerState(): VerticalScrollerState =
639 VerticalScrollerStateImpl()
640
641private class VerticalScrollerStateImpl(
642 scrollPosition: Int = 0,
643 scrollRange: Int = 0
644) : VerticalScrollerState {
645 private var _scrollPosition by
646 mutableStateOf(scrollPosition, structuralEqualityPolicy())
647
648 override var scrollPosition: Int
649 get() = _scrollPosition
650 set(value) {
651 _scrollPosition = value.coerceIn(0, scrollRange)
652 }
653
654 private var _scrollRange by
655 mutableStateOf(scrollRange, structuralEqualityPolicy())
656
657 override var scrollRange: Int
658 get() = _scrollRange
659 set(value) {
660 require(value >= 0) { "$value must be > 0" }
661 _scrollRange = value
662 scrollPosition = scrollPosition
663 }
664}
665
666@Composable
667fun VerticalScroller(
668 verticalScrollerState: VerticalScrollerState =
669 remember { VerticalScrollerState() }
670) {
671```
672
673**Jetpack Compose framework and Library development** SHOULD declare hoisted state types as interfaces instead of abstract or open classes if they are not declared as final classes.
674
675When designing an open or abstract class to be properly extensible for these use cases it is easy to create hidden requirements of state synchronization for internal consistency that are difficult (or impossible) for an extending developer to preserve. Using an interface that can be freely implemented strongly discourages private contracts between composable functions and hoisted state objects by way of Kotlin internal-scoped properties or functionality.
676
677**Jetpack Compose framework and Library development** SHOULD provide default state implementations remembered as default arguments. State objects MAY be required parameters if the composable cannot function if the state object is not configured by the caller.
678
679**Jetpack Compose framework and Library development** MUST NOT use `null` as a sentinel indicating that the composable function should internally `remember {}` its own state. This can create accidental inconsistent or unexpected behavior if `null` has a meaningful interpretation for the caller and is provided to the composable function by mistake.
680
681### Do
682
683```kotlin
684@Composable
685fun VerticalScroller(
686 verticalScrollerState: VerticalScrollerState =
687 remember { VerticalScrollerState() }
688) {
689```
690
691### Don't
692
693```kotlin
694// Null as a default can cause unexpected behavior if the input parameter
695// changes between null and non-null.
696@Composable
697fun VerticalScroller(
698 verticalScrollerState: VerticalScrollerState? = null
699) {
700 val realState = verticalScrollerState ?:
701 remember { VerticalScrollerState() }
702```
703
704## Default hoisted state for modifiers
705
706The `Modifier.composed {}` API permits construction of a Modifier factory that will be invoked later. This permits the associated Modifier factory function to be a "regular" (non-`@Composable`) function that can be called outside of composition while still permitting the use of composition to construct a modifier implementation for each element it is applied to. This does not permit using `remember {}` as a default argument expression as the factory function itself is not `@Composable`.
707
708**Jetpack Compose framework and library development** SHOULD provide an overload of Modifier factory functions that accept hoisted state parameters that omits the hoisted state object as a means of requesting default behavior, SHOULD NOT use null as a default sentinel to request the implementation to `remember {}` an element-instanced default, and SHOULD NOT declare the Modifier factory function as `@Composable` in order to use `remember {}` in a default argument expression.
709
710### Do
711
712```kotlin
713fun Modifier.foo() = composed {
714 FooModifierImpl(remember { FooState() }, LocalBar.current)
715}
716
717fun Modifier.foo(fooState: FooState) = composed {
718 FooModifierImpl(fooState, LocalBar.current)
719}
720```
721
722### Don't
723
724```kotlin
725// Null as a default can cause unexpected behavior if the input parameter
726// changes between null and non-null.
727fun Modifier.foo(
728 fooState: FooState? = null
729) = composed {
730 FooModifierImpl(
731 fooState ?: remember { FooState() },
732 LocalBar.current
733 )
734}
735```
736
737### Don't
738
739```kotlin
740// @Composable modifier factory functions cannot be used
741// outside of composition.
742@Composable
743fun Modifier.foo(
744 fooState: FooState = remember { FooState() }
745) = composed {
746 FooModifierImpl(fooState, LocalBar.current)
747}
748```
749
750## Extensibility of hoisted state types
751
752Hoisted state types often implement policy and validation that impact behavior for a composable function that accepts it. Concrete and especially final hoisted state types imply containment and ownership of the source of truth that the state object appeals to.
753
754In extreme cases this can defeat the benefits of reactive UI API designs by creating multiple sources of truth, necessitating app code to synchronize data across multiple objects. Consider the following:
755
756```kotlin
757// Defined by another team or library
758data class PersonData(val name: String, val avatarUrl: String)
759
760class FooState {
761 val currentPersonData: PersonData
762
763 fun setPersonName(name: String)
764 fun setPersonAvatarUrl(url: String)
765}
766
767// Defined by the UI layer, by yet another team
768class BarState {
769 var name: String
770 var avatarUrl: String
771}
772
773@Composable
774fun Bar(barState: BarState) {
775```
776
777These APIs are difficult to use together because both the FooState and BarState classes want to be the source of truth for the data they represent. It is often the case that different teams, libraries, or modules do not have the option of agreeing on a single unified type for data that must be shared across systems. These designs combine to form a requirement for potentially error-prone data syncing on the part of the app developer.
778
779A more flexible approach defines both of these hoisted state types as interfaces, permitting the integrating developer to define one in terms of the other, or both in terms of a third type, preserving single source of truth in their system's state management:
780
781```kotlin
782@Stable
783interface FooState {
784 val currentPersonData: PersonData
785
786 fun setPersonName(name: String)
787 fun setPersonAvatarUrl(url: String)
788}
789
790@Stable
791interface BarState {
792 var name: String
793 var avatarUrl: String
794}
795
796class MyState(
797 name: String,
798 avatarUrl: String
799) : FooState, BarState {
800 override var name by mutableStateOf(name)
801 override var avatarUrl by mutableStateOf(avatarUrl)
802
803 override val currentPersonData: PersonData =
804 PersonData(name, avatarUrl)
805
806 override fun setPersonName(name: String) {
807 this.name = name
808 }
809
810 override fun setPersonAvatarUrl(url: String) {
811 this.avatarUrl = url
812 }
813}
814```
815
816**Jetpack Compose framework and Library development** SHOULD declare hoisted state types as interfaces to permit custom implementations. If additional standard policy enforcement is necessary, consider an abstract class.
817
818**Jetpack Compose framework and Library development** SHOULD offer a factory function for a default implementation of hoisted state types sharing the same name as the type. This preserves the same simple API for consumers as a concrete type. Example:
819
820```kotlin
821@Stable
822interface FooState {
823 // ...
824}
825
826fun FooState(): FooState = FooStateImpl(...)
827
828private class FooStateImpl(...) : FooState {
829 // ...
830}
831
832// Usage
833val state = remember { FooState() }
834```
835
836**App development** SHOULD prefer simpler concrete types until the abstraction provided by an interface proves necessary. When it does, adding a factory function for a default implementation as outlined above is a source-compatible change that does not require refactoring of usage sites.
837