前の記事を含めて、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 のエンジニアもこの方法を推奨してないです。
むしろ、これはライブラリーの考えられた使い方より想定外のユースケースかもしれないので、IssueTracker や KotlinSlack で依頼してみましょう。
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(..))
/* .. */
}
-
当 ModifierElementNode の ClickableElement 以外も複数の Modifier に組み合わせて作られてることを確認できます。つまり、Modifier.clickable の実装を複製したいなら、組み合わせてる Modifier の実装も含めないといけない。
-
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で依頼をしてみました。
上記の通り、まだいくつかの課題が残っていますが、解決に向けて動きがすでに始めってるのでこれからもっと使いやすくなるかなと思ってます。
以上。