5
4

More than 1 year has passed since last update.

Modifier.Node を使いましょう (Part 6: Modifier.Node 移行しようと思ってまだ残ってる課題)

Last updated at Posted at 2023-09-10

前の記事を含めて、Modifier.composed のパフォーマンス問題の解決に向けて追加された Modifeir.Node の紹介と Modifier.composed を移行するチップスなどを紹介しました。

シリーズのコンテンツ
Part 1: なぜ Modifier.Node が必要になった
Part 2: Modifier.Node APIの紹介
Part 3: Modifier.Node に移行する基本手順
Part 4: @Composable 関数の実装を Modifier.Node に書き換える
Part 5: Modifier.Node を委任する、Delegate パターン
Part 6: Modifier.Node 移行しようと思ってまだ残ってる課題(この記事)

Modifier.composed の実装を Modifier.Node に書き換えたとき、最終的なコードは少し直感的ではないと冗長に個人的に感じてます。Modifier.focusable の移行の change list 見ればわかるかもしれない。

しかし、上記は完全に個人的な意見であり、これから Modifier.Node を使っていきたいや既存の Modifier を移行していきたいと思っていたら、客観的にまだいくつかの課題が残ってると思います。

private や internal になってる CompositionLocal

例えば、以下のような Modifier を Modifier.Node に移行してみると、

fun Modifier.toggleBackground() = composed {
    var selected by remember { mutableStateOf(false) }

    val color = if(selected) MaterialTheme.colorScheme.primary
                else MaterialTheme.colorScheme.inversePrimary

    return Modifier
        .clickable{ selected = !selected }
        .background(color = color)
}

MaterialTheme.colorScheme.primary の値を取得する必要があります。

MaterialTheme.colorScheme は実際に LocalColorScheme という CompositionLocal の値です。

object MaterialTheme {

    val colorScheme: ColorScheme
        @Composable
        @ReadOnlyComposable
        get() = LocalColorScheme.current
    ..
}

internal val LocalColorScheme = staticCompositionLocalOf{..}

上記のコードでご覧の通り、LocalColorScheme は実際に internal にスコープされてます。つまり、androidx ライブラリー以外は利用できないです。
このような private や internal にスコープされてるいくつか CompositionLocal があります。

@Suppress("INVISIBLE_MEMBER") を使って internal スコープを無視できるということは1つ手段です。

@Suppress("INVISIBLE_MEMBER")
class ToggleBackgroundNode: Modifier.Node(), .. {
    // エラー無視される
    val colorScheme
        = currentValueOf(LocalColorScheme)
    /* .. */
}

でも、internal にスコープされたことに関して開発者の理由もありますし、Google のエンジニアもこの方法を推奨してないです。

むしろ、これはライブラリーの考えられた使い方より想定外のユースケースかもしれないので、IssueTrackerKotlinSlack で依頼してみましょう。

Delegate できる Modifier.Node が少ない

また上記の例を使わせていただきます。

fun Modifier.toggleBackground() = composed {
    var selected by remember { mutableStateOf(false) }

    val color = if(selected) /* .. */ 
                else /* .. */

    return Modifier
        .clickable{ selected = !selected }
        
..
}

基本的にタップする時ラムダーが実行されるなので、単なる SuspendingPointerInputModifierNode にタップ処理を委任するような実装できます。

class ToggleBackgroundModifierNode()
    : DelegatingNode(), PointerInputModifierNode {
    private var selected = false

    val pointerInputNode = delegate(
        SuspendingPointerInputModifierNode {
            detectTapGestures(
                onTap = { this.selected = !this.selected }
            )
        }
    )
}

直接的にユースケースを満たせることができてますが、Modifier.clickable の内部実装を見ると、

fun Modifier.clickable(..) {
    // [1]
    return Modifier
        .indication(..)
        .hoverable(..)
        .focusableInNonTouchMode(..)
        .then(ClickableElement(..))
}

class ClickableNode(..)
    : DelegatingNode(), PointerInputModifierNode {

    // [2]
    val clickableSemanticsNode = delegate(ClickableSemanticsNode(..))
    /* .. */
}
  1. 当 ModifierElementNode の ClickableElement 以外も複数の Modifier に組み合わせて作られてることを確認できます。つまり、Modifier.clickable の実装を複製したいなら、組み合わせてる Modifier の実装も含めないといけない。

  2. ClickableSemanticsNode という SemanticsNodeModifeir に拡張してる Modifier.Node を delegate してます。この Modifier.Node の実装も複製しないといけないです。

それぞれの Modifier の実装を複製してみてもシンプルな Modifier.clickable のため実装の複雑さを増やす意味がないかもしれないですね。

理想的に SuspendingPointerInputModifierNode と同様で、private スコープされてる ClickableNode のインスタンスを取得できるようなメソッドがあれば、いろんなユースケースを既に満たせて実装されてる ClickableNode を delegate しやすくなりますね。例えば、

class ToggleBackgroundModifierNode()
    : DelegatingNode(), PointerInputModifierNode {
    private var selected = false

    val pointerInputNode = delegate(
        ClickableNode(onClick = { this.selected = !this.selected })
    )
}

// 理想案です!実際に存在してないコードです!
fun ClickableNode(onClick: () -> Unit): ClickableNode
    = ClickableNode(onClick)

この記事を書いたとき、Delegate できるためインスタンスを取得できる Modifier.Node は以下のみになってます:

上記の課題と同じ、IssueTrackerで依頼をしてみました。


上記の通り、まだいくつかの課題が残っていますが、解決に向けて動きがすでに始めってるのでこれからもっと使いやすくなるかなと思ってます。

以上。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4