こちらにあるドキュメントです。
ある程度Composeを触ったことがある人向けのドキュメントだと思います。開発チームなどでComposeやっていく人は一読の価値があるかなと思いました。
The 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.
Jetpack Compose API をidiomaticに記述するためのパターン、ベストプラクティス、スタイルガイド。Jetpack Composeのコードはレイヤーで構築されるため、Jetpack Composeを利用する人は全員が自分自身が使うためのAPIを構築している。
RFC2119というするべきの基準が使われている
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.
RFC において要請の程度を示すために用いるキーワードが定義されています。
https://www.ipa.go.jp/security/rfc/RFC2119JA.html
- 「しなければならない( MUST )」 English
この語句、もしくは「要求されている( REQUIRED )」および「することになる( SHALL )」は、その規定が当該仕様の絶対的な 要請事項であることを意味します。
- 「する必要がある( SHOULD )」
この語句もしくは「推奨される( RECOMMENDED )」という形容表現は、 特定の状況下では、特定の項目を無視する正当な理由が存在するかもしれませんが、 異なる選択をする前に、当該項目の示唆するところを十分に理解し、 慎重に重要性を判断しなければならない、ということを意味します。
- 「してもよい( MAY )」
この語句、もしくは「選択できる( OPTIONAL )」という形容表現は、ある要素が、まさに選択的であることを意味します。
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.
ちなみにこのドキュメントでは何も書いていない場合はOPTIONALになるそうです。
Kotlin Style
Kotlin Coding Conventionsをベースにする
https://kotlinlang.org/docs/reference/coding-conventions.html
Singletons, constants, sealed class and enumの命名をPascalCaseに
Jetpackフレームワーク開発: MUST
ライブラリ開発: SHOULD
アプリ開発: MAY
なぜ?
Jetpack 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.
singletonsやcompanion object stateの有効性が低下するので、見分ける必要が低下するため。
一貫性、実装詳細を隠すため。
Don't
const val DEFAULT_KEY_NAME = "__defaultKey"
enum class Status {
IDLE,
BUSY
}
Do
const val DefaultKeyName = "__defaultKey"
enum class Status {
Idle,
Busy
}
Compose baseline
Unit型のComposable関数はPascalCaseで、名詞でなければならない
Jetpackフレームワーク開発とライブラリ開発: MUST
アプリ開発: SHOULD
なぜ?
Composable 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.
declarative entitiesとしてみなされるので、クラスの命名規則に従うため。(Composable関数を宣言として扱いたいみたい。)
Don't
// This function is PascalCased but is not a noun!
// この関数はPascalCaseだけど、名詞じゃない!
@Composable
fun RenderFancyButton(text: String, onClick: () -> Unit) {
Do
// This function is a descriptive PascalCased noun as a visual UI element
// この関数はPascalCaseで表示されるUI elementとしての名詞である
@Composable
fun FancyButton(text: String, onClick: () -> Unit) {
値を返すComposable関数はKotlinの命名規則に従う※
Jetpackフレームワーク開発とLibrary development: MUST
アプリ開発については記述なし
つまり小文字で関数を始める。
ただし、Kotlin Coding Conventionsのfactory function conventionのパターンには従ってはならない。(つまり大文字でFactory関数を作るべきではない。)
https://kotlinlang.org/docs/coding-conventions.html#function-names
なぜ?
factory function convention(関数名を大文字から始めてインスタンスを作って返す規則)を使うとユーザーに不適切な期待をもたせてしまう。
例えば、CompositionLocalを使って値を返す場合は、ファクトリ関数名で表現する必要がある目に見えない入力がある。
例えば、rremember{}でキャッシュしたりする場合は、コンストラクタ呼び出しのように見える。
また、Unitを返すComposable関数との混乱も起こる。
Do
// Returns a style based on the current CompositionLocal settings
// This function qualifies where its value comes from
// 今のCompositionLocalの設定をベースにしたstyleを返す。
// この関数はどこから値が来るかを修飾する
@Composable
fun defaultStyle(): Style {
Don't
// Returns a style based on the current CompositionLocal settings
// This function looks like it's constructing a context-free object!
// この関数はcontext-free objectを作るように見える!
@Composable
fun Style(): Style {
remember{}したオブジェクトを返す@Composable
関数はrememberをプレフィックスにつける
なぜ?
呼び元にわかりやすくする。ないと呼び元で重複してremember呼んだりする。
Do
// Returns a CoroutineScope that will be cancelled when this call
// leaves the composition
// This function is prefixed with remember to describe its behavior
@Composable
fun rememberCoroutineScope(): CoroutineScope {
Don't
// Returns a CoroutineScope that will be cancelled when this call leaves
// the composition
// This function's name does not suggest automatic cancellation behavior!
// この関数の名前は自動的にcancelされることを想起しない!
@Composable
fun createCoroutineScope(): CoroutineScope {
CompositionLocalの命名は後ろにCompositionLocalやLocalをつけてはならない、前にLocalをつけてもよい。
Jetpackフレームワーク開発とライブラリ MUST NOT : 命名の後ろにCompositionLocalやLocalをつけてはならない
Jetpackフレームワーク開発とライブラリ MAY : 命名の前に"Local"をつけても良い
Do
// "Local"が形容詞として使われている, "Theme"は名詞.
val LocalTheme = staticCompositionLocalOf<Theme>()
Don't
// "Local"が名詞として使われている!
val ThemeLocal = staticCompositionLocalOf<Theme>()
Stable type
結構難しいので、原文と訳を載せています。
The 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.
Compose runtimeは型や関数に2つの stable
を示すアノテーションを公開している。 stable
は最適化をCompose compilerが安全に行うことができるもの。 どのような最適化かというと、safe typeは入力がわからない限りは変わらないので、Compose runtimeはsafe typesのみ関数呼び出しをスキップするようなもの。 (ここもここ以外もそこまで翻訳自信ないので、変更リクエストください。)
The 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."
Compose compilerはこれらの型のプロパティを自動的に推測する、安定性(stability)が推定できないインターフェースや他の型はアノテーションをつけることができる。これらの型を総称して、 stable types
と呼ぶ。
@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.
@Immutable
はオブジェクトが作られたら、すべてのプロパティが変わらないこと、すべてのメソッドは参照透過(referentially transparent)つまり挙動が変わらないことを示す。Kotlinのconstで使われる型(プリミティブ型やString)は @Immutable
と見なす。
@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.
Stableは型が可変であることを示すが、publicなプロパティやメソッドの結果が異なった場合はComposeランタイムに通知される。(実際にはMutableStateによるSnapshotシステムによって通知される。) @Stable
や@Immutable
のプロパティを持つ型のみバックする。(ここでのbackの意味がよく分からず)
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.
Jetpackフレームワーク開発とライブラリとアプリ開発: MUST .equals()の実装でaとbに対して常に同じ値を返す必要がある。将来的にaの変更がbにも反映されなくてはならないことを示し、逆も同様。
This 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.
この制約はequals()のデフォルト実装の ===による実装では常に正しい実装になる。
Jetpack Compose framework development and Library development SHOULD correctly annotate @Stable and @Immutable types that they expose as part of their public API.
Jetpackフレームワーク開発とライブラリ開発: SHOULD public APIとして公開する型として公開する場合は正しく @Stable
や @Immutable
のアノテーションを付ける
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.
Jetpackフレームワーク開発とライブラリ開発: MUST NOT 前のStableリリースで @Stable
や @Immutable
のアノテーションにつけていた場合はアノテーションを削除してはならない。
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.
Jetpackフレームワーク開発とライブラリ開発: MUST NOT 前のStableリリースで@Stable
や @Immutable
のアノテーション無しでリリースしていた場合、アノテーションを追加してはならない。
なぜ?
@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.
@Stable
や @Immutable
は バイナリ互換性に影響を与える行動契約(= behavioral contracts)である。 ライブラリは既存の正しく実装されていない可能性のある非final型に契約を追加してはいけない。同様に既存のコードが依存している可能性のある型が契約に従わなくなったことを宣言してはいけない。
Implementing 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.
@Stable
や@Immutable
がついた型に対してstable契約を誤って実装し、パラメーターやレシーバーとして型を受け取ると @Composable
関数は誤った挙動になる。
@Composable
関数はコンテンツをemitするか値を返すかのどちらかだけをする
Jetpack Compose framework development and Library development MUST NOT expose any single @Composable function that both emits tree nodes and returns a value.
Jetpackフレームワーク開発とライブラリ開発: MUST NOT コンテンツをemitするのと値を返すのを両方を行う@Composable
関数を公開する
なぜ?
コンテンツのemitは順番に行われる必要があり、返り値を使うということはその順番を制限することになるため。
Do
@Composable
fun InputField(inputState: InputState) {
// ...
// Communicating with the input field is not order-dependent
val inputState = remember { InputState() }
Button("Clear input", onClick = { inputState.clear() })
InputField(inputState)
Don't
// Emits a text input field element and returns an input value holder
@Composable
fun InputField(): UserInputState {
// ...
// Communicating with the InputField is made difficult
// InputFieldと連携するのが難しくなる
Button("Clear input", onClick = { TODO("???") })
val inputState = InputField()
Compose UI API structure
Compose UI elements
Compose UI tree nodeをemitする @Composable
関数をelementと呼ぶ。
Jetpackフレームワーク開発とライブラリ開発: MUST このセクションに従うこと
アプリ開発: SHOULD このセクションに従うこと
Element関数はUnitを返す
利用できない要素はパラメーターから提供する。
なぜ?
宣言的entityであるので、値を返す必要がないため。
emitする要素はパラメーターによって制御されるべきであり、Element関数の返り値によって制御されるべきではない。
Do
@Composable
fun FancyButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Don't
interface ButtonState {
val clicks: Flow<ClickEvent>
val measuredSize: Size
}
@Composable
fun FancyButton(
text: String,
modifier: Modifier = Modifier
): ButtonState {
ElementはModifier型のパラメーターを持ち、尊重する
Element 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関数はModifier型のパラメーターを持ち、名前をmodifierで、最初のオプショナルパラメーターでないといけない。(MUST)
If 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.
Element関数が最小サイズを持つ場合、つまり、minWidth と minHeightに0を指定した場合に0でない値を返す場合にはempty Modifierを引数として取らなくてはならない。(MUST) canvasなど測定可能(measurable)なコンテンツを持たないElement関数はmodifierパラメーターを必要として、デフォルト値を省略しても良い。
Element 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.
Element関数で表示するrootのElement関数に渡すことによって、Compose UIノードにパラメーターに引数で渡ってきたmodifierパラメーターを渡さなくてはならない。 (MUST) Elementが直接nodeをemitするときはそのノードにmodifierを渡さなくてはならない。 (MUST)
Element 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.
Element関数はに引数で受け取ったmodifierの最後にmodifierを追加してからCompose nodeに渡しても良い。 (MAY)
Element 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.
Element関数は受け取ったmodifierの最初にmodifierを追加してはならない。(MUST NOT)
なぜ?
Modifiers 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.
Modifierは外部の振る舞いを追加する標準な方法で、これはelementのAPIをもっと小さくフォーカスしたものにできる。
An 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.
標準な方法でmodifierを受け入れないElement関数は、Elementへの変更を阻止するのではなく、非効率的なより深いツリー構造を持つ、非効率的なUIコードを書くことを余儀なくする。
Modifiers 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.
Modifierは最初のオプションパラメーターとして常にあることによって、常に最後の位置でmodifierを提供できるという期待を設定している。
See the Compose UI modifiers section below for more details.
詳しくは Compose UI modifiers section で
Do
@Composable
fun FancyButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) = Text(
text = text,
modifier = modifier.surface(elevation = 4.dp)
.clickable(onClick)
.padding(horizontal = 32.dp, vertical = 16.dp)
)
Compose UI layouts
A Compose UI element that accepts one or more @Composable function parameters is called a layout.
一つまたは複数の@Composable
関数をパラメーターとして取るelementをlayoutと呼ぶ。
例:
@Composable
fun SimpleRow(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Jetpack Compose framework development and Library development MUST follow all guidelines in this section.
Jetpackフレームワーク開発とライブラリ開発: MUST このガイドラインセクションに従う。
Jetpack Compose app development SHOULD follow all guidelines in this section.
アプリ開発: SHOULD このガイドラインセクションに従う。
Layout functions SHOULD use the name "content" for a @Composable function parameter if they accept only one @Composable function parameter.
Layout関数は 一つだけ@Composable
関数のパラメーターを取る場合、"content"という名前を@Composable
関数のパラメーターとして使わなくてはならない。(SHOULD)
Layout functions SHOULD use the name "content" for their primary or most common @Composable function parameter if they accept more than one @Composable function parameter.
Layout関数は複数@Composable
関数のパラメーターを取る場合、それらの主要または最も一般的な@Composable
関数のパラメーターに"content"という名前を使わなくてはならない。(SHOULD)
Layout 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.
Layout関数は Kotlinのtrailing lambda syntaxを使うことができるようにするために content
の@Composable
関数のパラメーターの位置を最後にしなくてはならない。
Compose UI modifiers
A 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.
(Modifier自体の説明なので省略)
Jetpack Compose framework development and Library development MUST follow all guidelines in this section.
Jetpackフレームワーク開発とライブラリ開発: MUST このガイドラインセクションに従う。
Modifier factory functions
Modifier chains are constructed using a fluent builder syntax expressed as Kotlin extension functions that act as factories.
Kotlin extension functionとして表現される、Factoryとして動く、fluent builder syntaxを使ってModifier chainが作られる。
Modifier.preferredSize(50.dp)
.backgroundColor(Color.Blue)
.padding(10.dp)
Modifier APIs MUST NOT expose their Modifier.Element interface implementation types.
Modifier APIはModifier.Elementのinterfaceの実装を公開してはならない。 (MUST NOT)
Modifier APIs MUST be exposed as factory functions following this style:
ModifierのAPIは以下の形で公開されなくてはならない。 (MUST)
fun Modifier.myModifier(
param1: ...,
paramN: ...
): Modifier = then(MyModifierImpl(param1, ... paramN))
Composed modifiers
Modifiers 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:
Compositionに参加しなければいけないComposition (例えば、CompositionLocalの値を読んだり、element特有の状態を保持したり、lifecycleを管理したり)は Modifier.composed {}
が使える。
fun Modifier.myModifier(): Modifier = composed {
val color = LocalTheme.current.specialColor
backgroundColor(color)
}
Composed 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:
以下のようにmodifierを使いまわした場合にも、それぞれcomposition stateが作られる。
fun Modifier.modifierWithState(): Modifier = composed {
val elementSpecificState = remember { MyModifierState() }
MyModifier(elementSpecificState)
}
// ...
val myModifier = someModifiers.modifierWithState()
Text("Hello", modifier = myModifier)
Text("World", modifier = myModifier)
As 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.
Jetpackフレームワーク開発とライブラリ開発: SHOULD そのため、 composition-awareなmodifireを実装するために Modifier.composed {}
を使わなくてはならない。また、 modifier extension factory functionsを作る時にその関数自体を @Composable
関数にしてはならない(SHOULD NOT)。
なぜ?
Composed modifierはcompositionの外で作成したり、element間で共有されたり、top-level定数として宣言することができる。これにより、 @Composable関数
の呼び出しでしか作成できないモディファイアよりも柔軟性が高くなり、誤ってelement間で状態を共有してしまうことを防ぐことができるため。
Layout-scoped modifiers
Android'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.
AndroidのViewにはLayoutParamsという概念があり、ViewGroupにViewGroupの子ViewがViewGroup固有の測定と配置のレイアウトの指示を提供する。
Compose UI modifiers afford a related pattern using ParentDataModifier and receiver scope objects for layout content functions:
Compose UIも同様のパターンを行うことができ、ParentDataModifierとreceiver scope objectsをレイアウトのcontent関数に使うことによりおこなう。
@Stable
interface WeightScope {
fun Modifier.weight(weight: Float): Modifier
}
@Composable
fun WeightedRow(
modifier: Modifier = Modifier,
content: @Composable WeightScope.() -> Unit
) {
// ...
// Usage:
WeightedRow {
Text("Hello", Modifier.weight(1f))
Text("World", Modifier.weight(2f))
}
Jetpackフレームワーク開発とライブラリ開発: SHOULD スコープ付きのModifier factory関数を使用して、親レイアウト composableに固有のmodifierを提供するべき。
Compose API design patterns
stateless
でcontrolled
な@Composable
関数を使おう
In 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.
この文脈でのstateless
は @Composable
関数自体が状態を持たずに、外部の状態パラメーターを受け入れる@Composable
関数を指す。 controlled
とは呼び出し側がComposableに提供される状態を完全にコントロールするという考え方。
Do
@Composable
fun Checkbox(
isChecked: Boolean,
onToggle: () -> Unit
) {
// ...
// Usage: (caller mutates optIn and owns the source of truth)
// 呼び出し元が、optionを変更し、source of truthとなっている。
Checkbox(
myState.optIn,
onToggle = { myState.optIn = !myState.optIn }
)
Don't
@Composable
fun Checkbox(
initialValue: Boolean,
onChecked: (Boolean) -> Unit
) {
var checkedState by remember { mutableStateOf(initialValue) }
// ...
// Usage: (Checkbox owns the checked state, caller notified of changes)
// Caller cannot easily implement a validation policy.
// Callerは簡単にはvalidationを実装することができない
Checkbox(false, onToggled = { callerCheckedState = it })
stateとeventを分けよう
Compose'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.
ComposableのmutableStateOf()の値の保持する仕組みはSnapshotシステムを通じて観測可能であり、変更を観測者に知らせる。これはrecomposition、再レイアウトや再描画の主要なメカニズムである。この状態の観測と効果的に使用するには状態とイベントの違いを認識する必要がある。
An 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.
Observable 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.
(状態とイベントの違いについて説明しているので省略)
Compose 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.
Composeはイベントではなく状態を入力として操作する。 Composable関数は関数の引数と mutableStateOf()
を入力とするオブザーバーである。
Hoisted state types
A 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.
stateless parameterと複数のイベントコールバックパラメーターパターンは規模が大きくなると扱いづらくなる。ステートとコールバックのコレクションをインターフェイスにまとめるのが適切になり、呼び出し側がまとまったポリシーオブジェクトを単位として提供できるようになる。
Before
@Composable
fun VerticalScroller(
scrollPosition: Int,
scrollRange: Int,
onScrollPositionChange: (Int) -> Unit,
onScrollRangeChange: (Int) -> Unit
) {
After
@Stable
interface VerticalScrollerState {
var scrollPosition: Int
var scrollRange: Int
}
@Composable
fun VerticalScroller(
verticalScrollerState: VerticalScrollerState
) {
In 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.
上記の例では、VerticalScrollerState の実装は、var プロパティのcustom getter/setterを使用して、ポリシーを適用したり、状態自体の保存を別の場所にデリゲートすることができる。
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.
Jetpackフレームワーク開発とライブラリ開発: SHOULD 相互に関連したポリシー(interrelated policy)の収集とグループ化するために hoisted state type
を宣言すべき。例えば、VerticalScrollerStateの例では、scrollPositionとscrollRangeの間で依存関係を示している。内部の一貫性を維持するために、scrollPositionを有効な範囲にするために、変更したりエラーをレポートする必要がある。これらの一貫性を処理するためにグループ化する必要がある。
Jetpack Compose framework and Library development SHOULD declare hoisted state types as @Stable and correctly implement the @Stable contract.
Jetpackフレームワーク開発とライブラリ開発: SHOULD hoisted state typeを@Stable
として宣言し、 @Stable
契約を正しく実装するべき。
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".
Jetpackフレームワーク開発とライブラリ開発: SHOULD hoisted state typeの名前には、コンポーザブル関数の名前に "State "という名前を後ろにつける。
Default hoisted state for modifiers
The 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.
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.
composed{}である場合はそのmodifierの関数自体は @Composable
にしないので、デフォルト引数でremember{}
できない。
Jetpackフレームワーク開発とライブラリ開発: SHOULD remember{}
によるデフォルトの挙動を提供するオーバーロードを提供すべき。
SHOULD NOT nullをデフォルト値として remember {}
と切り替えてはならない。
SHOULD NOT Modifier factory関数は remember{}
をデフォルト引数で使うために @Composable
関数にしてはならない。
Do
fun Modifier.foo() = composed {
FooModifierImpl(remember { FooState() }, LocalBar.current)
}
fun Modifier.foo(fooState: FooState) = composed {
FooModifierImpl(fooState, LocalBar.current)
}
Don't
// Null as a default can cause unexpected behavior if the input parameter
// changes between null and non-null.
// nullとnullじゃない挙動が変わったとき、予期しない挙動を起こす。
fun Modifier.foo(
fooState: FooState? = null
) = composed {
FooModifierImpl(
fooState ?: remember { FooState() },
LocalBar.current
)
}
Don't
// @Composable modifier factory functions cannot be used
// outside of composition.
// @Composable modifierがcomposition外から利用できない。
@Composable
fun Modifier.foo(
fooState: FooState = remember { FooState() }
) = composed {
FooModifierImpl(fooState, LocalBar.current)
}
## hoisted state typeの拡張性
Hoisted 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.
Hoisted state typeを受け入れるComposable関数の動作に影響を与えるポリシーやバリデーションをHoisted state typeに実装することがある。final hoisted state typeはstate objectがアピールするsource of truthの束縛と所有権を意味する。
In 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:
極端なケースでは、複数のオブジェクトにデータが同期するコードを余儀なくされたときに、これは複数のsource of truthを作ることによってreactive UI API designの利点が損なわれることになる。
// Defined by another team or library
// 別のチームやライブラリによって定義されている
data class PersonData(val name: String, val avatarUrl: String)
class FooState {
val currentPersonData: PersonData
fun setPersonName(name: String)
fun setPersonAvatarUrl(url: String)
}
// Defined by the UI layer, by yet another team
// UI layerで定義されているが別チームによって実装されている
class BarState {
var name: String
var avatarUrl: String
}
@Composable
fun Bar(barState: BarState) {
These 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.
FooStateもFooStateもsource of truthになろうとしているため、一緒に使うことが困難。システム間で共有しなければならないデータについて単一の統一された型に合意するという選択肢がないことがよくある。このような設計はエラーを起こしやすいデータ同期の要件を形成する。
A 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:
より柔軟なアプローチでは、これらのhoisted state typeをinterfaceとして定義して、統合部分を開発する開発者が一方をもう一方のタイプで定義したり、両方を別のタイプで定義することを可能にして、システムの状態管理におけるsingle source of truthを保持する。
@Stable
interface FooState {
val currentPersonData: PersonData
fun setPersonName(name: String)
fun setPersonAvatarUrl(url: String)
}
@Stable
interface BarState {
var name: String
var avatarUrl: String
}
class MyState(
name: String,
avatarUrl: String
) : FooState, BarState {
override var name by mutableStateOf(name)
override var avatarUrl by mutableStateOf(avatarUrl)
override val currentPersonData: PersonData =
PersonData(name, avatarUrl)
override fun setPersonName(name: String) {
this.name = name
}
override fun setPersonAvatarUrl(url: String) {
this.avatarUrl = url
}
}
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.
Jetpackフレームワーク開発とライブラリ開発: SHOULD hoisted state typeをinterfaceとして定義する。他のpolicyの強制が必要であれば、abstract classを検討する。
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:
Jetpackフレームワーク開発とライブラリ開発: SHOULD hoisted state typeのデフォルト実装のための型と同じ名前で提供するファクトリ関数を提供するべき。これによって利用者にシンプルなAPIが維持される。
@Stable
interface FooState {
// ...
}
fun FooState(): FooState = FooStateImpl(...)
private class FooStateImpl(...) : FooState {
// ...
}
// Usage
val state = remember { FooState() }
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.
アプリ開発 : SHOULD インターフェースによる抽象化が必要になるまでは、よりシンプルな具象型を使っていきましょう。必要になった場合にデフォルト実装のためのファクトリ関数を追加することはソース互換性のある変更なので、利用側でリファクタリングする必要がない。