現代のAndroid開発において、Jetpack ComposeはAndroid向けUI構築のためのモダンなツールキットであるだけでなく、Kotlin Multiplatformプロジェクトもサポートする存在へと進化しています。そのエコシステムは急速に拡大しており、いまやAndroid開発に欠かせない要素となっています。

Composeの中心にあるのが ModifierModifierは単なるスタイルやテーマを適用する仕組みではなく、コンポーネント同士を独立させつつ、柔軟にカスタマイズできるようにするデザインパターンです。カスタムModifierを作成することで、一度定義した動作をアプリ全体で再利用でき、UIコードをよりクリーンかつ柔軟に保つことができます。

この記事では、 Modifier.then()Modifier.composed()、そして Modifier.Node、という3つの主要なAPIを使ってカスタムModifierを作成する方法を紹介し、さらに RevenueCat’s Android SDKModifier.Node を活用してUIパフォーマンスを向上させている仕組みについて解説します。

Modifier.then

Modifier.then() 関数を使うのは、カスタムModifierを作成する最も簡単な方法です。この関数を使うと、複数のModifierをチェーンでつなぎ、最終的な結果として結合されたModifierを返すことができます。以下の例のように使用します。

1@Composable
2fun Modifier.composableModifier(): Modifier {
3    val color = LocalContentColor.current.copy(alpha = 0.5f)
4    return this then Modifier.background(color)
5}
6
7@Composable
8fun MyComposable() {
9    val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher
10}

infix fun Modifier.then(other: Modifier) は、Jetpack ComposeにおけるModifierのチェーン処理を担う基本的な演算子です。この関数は単なるフラットなリストを作るのではなく、 CombinedModifier. と呼ばれる再帰的な2要素のデータ構造を生成します。この構造は、Compose UIツールキットが必要とする特定の方向性をもった走査パターンに最適化された、連結リスト(consリスト)のような仕組みとして機能します。

例えば、UI要素のプロパティを構築するために、開発者が宣言的にModifierをチェーンできるようにするシンプルなModifierがあるとしましょう。

1Modifier
2    .padding(16.dp)      // First (outer)
3    .background(Color.Blue) // Second
4    .clickable { }       // Third (inner)

多くの人は、これで List<Modifier.Element>が作られると想像するかもしれません。しかし実際にはそうではありません。チェーンの各Modifierが暗黙に呼び出す Modifier.then() は、入れ子の木構造を組み立てます。この設計は、パフォーマンスと正確性のために意図的に選ばれた重要なアーキテクチャ上の判断です。

Modifier.then() 関数の内部コードを確認すると、この関数が内部的に新しい CombinedModifier インスタンスを生成していることがわかります。

1
2infix fun then(other: Modifier): Modifier =
3    if (other === Modifier) this else CombinedModifier(this, other)
4        
5 class CombinedModifier(internal val outer: Modifier, internal val inner: Modifier) : Modifier {
6    override fun <R> foldIn(initial: R, operation: (R, Modifier.Element) -> R): R =
7        inner.foldIn(outer.foldIn(initial, operation), operation)
8
9    override fun <R> foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
10        outer.foldOut(inner.foldOut(initial, operation), operation)
11
12    override fun any(predicate: (Modifier.Element) -> Boolean): Boolean =
13        outer.any(predicate) || inner.any(predicate)
14
15    override fun all(predicate: (Modifier.Element) -> Boolean): Boolean =
16        outer.all(predicate) && inner.all(predicate)
17
18    override fun equals(other: Any?): Boolean =
19        other is CombinedModifier && outer == other.outer && inner == other.inner
20
21    override fun hashCode(): Int = outer.hashCode() + 31 * inner.hashCode()
22
23    override fun toString() =
24        "[" +
25            foldIn("") { acc, element ->
26                if (acc.isEmpty()) element.toString() else "$acc, $element"
27            } +
28            "]"
29}

この関数の目的は、既存のチェーン (this) と新しいModifier (other), and join them using theという2つのModifierを受け取り、それらを CombinedModifier クラスを使って結合することです。 CombinedModifier はコレクションではなく、単純な2要素のコンテナであり、リンクドリストのノードとして機能します。

1class CombinedModifier(internal val outer: Modifier, internal val inner: Modifier) : Modifier
  • outer: チェーン内で*先に*追加されたModifierを保持します。
  • inner: 新たに追加されたModifierを保持します。

たとえば、 Modifier.padding(8.dp).background(Color.Red)と書くと、生成される構造は次のようになります:CombinedModifier(outer = CombinedModifier(outer = padding, inner = background), inner = clickable)

これは、左結合(left-associative)の再帰的な構造を作り出しており、 ((padding then background) then clickable)のような形になります。

この「リンクドリスト」構造こそが、Modifierがどのように処理されるかを理解する鍵です。 CombinedModifier の仕組みは、その双方向の走査メソッドによって実現されています。通常のリストが「先頭から末尾」へ1方向にしか走査できないのに対し、CombinedModifier はUIパイプラインの異なるフェーズに対応するため、逆方向にも走査可能です。

つまり、 Modifier.then は、シンプルでありながら最適化された関数であり、宣言的かつチェーン可能なModifierシステム全体を支える中核的な仕組みとなっています。

Modifier.composed

もうひとつ利用できるAPIが Modifier.composed()です。この関数を使うと、ラムダ式内で Composable関数 を利用できるカスタムModifierを作成できます。

1fun Modifier.myStatefulModifier() = composed {
2    val myState by remember { mutableStateOf(0f) }
3    // ... use myState
4    Modifier.drawBehind { /* ... */ }
5}

内部的な動作は Modifier.then とよく似ています。 ComposedModifier は新しい ComposedModifier インスタンスを生成し、それを既存のModifierと Modifier.then を使って結合します。以下の例のように動作します。

1fun Modifier.composed(
2    inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
3    factory: @Composable Modifier.() -> Modifier,
4): Modifier = this.then(ComposedModifier(inspectorInfo, factory))

Modifier.composed はカスタムModifierを簡単に作成できる便利なAPIですが、パフォーマンス面での問題があるため、現在は推奨されていません。このメソッドでは、remember などの @Composable 関数 をModifierインスタンス作成時に呼び出すことができます。これにより、特定インスタンス専用の状態(state)を保持でき、同じModifierオブジェクトを複数箇所で安全に再利用できるという利点があります。

しかし、実際に @Composable 関数を呼ばずに Modifier.composed を使用すると、不要なオーバーヘッドが発生します。
この場合、Modifierはスキップ可能(skippable)と見なされなくなり、再コンポジション時に追加の処理が強制されるため、毎回Modifierチェーン全体を再実行して状態を更新する必要が生じます。 結果として、UIの再コンポジション時のパフォーマンスが低下する原因となります。

Modifier.Node:状態とロジックの中核

Modifier.Node は Compose UI 1.3.0 から導入されたAPIで、現在では Modifier.composedに代わるカスタムModifier作成の推奨手段となっています。従来の Modifier.composed は、状態を持つカスタムModifierを作成する初期の仕組みとして提供されていましたが、各Modifierインスタンスごとに新しいサブコンポジションを生成する必要があるという大きなパフォーマンスコストがありました。このオーバーヘッドは、特にリストなどの動的UIにおいてパフォーマンス低下を引き起こす原因となっていました。

この課題を直接解決するために登場したのが Modifier.Node です。これはComposeのレンダリングパイプラインに統合された軽量でライフサイクル管理に対応した低レベルAPIであり、サブコンポジションのオーバーヘッドなしに状態を持つModifierを作成できるようになっています。

Modifier.Node

Modifier.Node の実装は、カスタムModifierの中核部分となります。
これは状態を持つ可変クラスであり、Modifierのプロパティを保持し、その動作ロジックを実装します。重要なのは、 Modifier.Node のインスタンスが複数回の再コンポジションをまたいで保持されること、そして設定が変わらない場合には再利用も可能であるという点です。

1// The Node: Holds state (color) and logic (drawCircle)
2private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
3    override fun ContentDrawScope.draw() {
4        // Implements the drawing logic
5        drawCircle(color)
6    }
7}

Element には主に次の2つの役割があります:

1.  create():Modifierが最初に適用されたときに、 Modifier.Node のインスタンスを生成します。

2.  update(node: CircleNode):これがパフォーマンスの鍵です。再コンポジション時に Element が変更された( equals メソッドによって判定される)場合、既存の Node インスタンスに対してこの関数が呼び出されます。これにより、新しいオブジェクトを生成せずに Node の状態を更新できます。

補足: ModifierNodeElement では、 equalshashCode を正しく実装することが非常に重要です。
最も簡単な方法は data class を使用することです。これらを正しく実装しないと、 update が不要に呼ばれ、パフォーマンス上の利点が失われてしまいます。

Modifier ファクトリー

これは、開発者が利用するためのシンプルで明確な公開API関数です。この関数では、 then 演算子を使って、新しい ModifierNodeElement を既存のModifierチェーンに連結します。

1// The Factory: The clean public API
2fun Modifier.circle(color: Color): Modifier = this.then(CircleElement(color))

これで、作成したカスタムModifierは、他の組み込みModifier関数と同じように利用できるようになります。ステートレスなファクトリー(Element)とステートフルなワーカー(Node)を分離することで、オブジェクトの生成を最小限に抑え、再利用性を最大化します。さらに、それが持つ豊富なノードタイプとライフサイクル対応APIによって、UIパイプラインのあらゆるフェーズに直接的かつ安全で効率的なアクセスが可能になります。

RevenueCat’s Android SDK でも、より高いUIパフォーマンスを実現するために Modifier.Node を活用しています。最近、プレースホルダー機能の実装において、従来の Modifier.composed() から Modifier.Node へ移行しました。詳細や実際のコード例は、Placeholder.kt file で確認できます。

まとめ

この記事では、 Modifier.then()Modifier.composed()、そして Modifier.Node の3つの主要APIを使ってカスタムModifierを作成する方法を紹介し、さらに RevenueCat SDKModifier.Node を活用してUIパフォーマンスを向上させている仕組みを解説しました。それぞれのアプローチにはトレードオフがあります。 Modifier.then() シンプルで軽量ですが、 Modifier.composed() は誤用するとパフォーマンス低下を招く可能性があります。一方、 Modifier.Node はよりモダンで効率的な方法を提供し、柔軟かつパフォーマンスに優れたカスタム動作を実現できます。これらのパターンを正しく理解し、意図的に使い分けることで、再利用性が高く、表現力があり、パフォーマンスにも優れたCompose UIを構築することができるでしょう。

いつものように、ハッピーコーディングを!

— Jaewoong