Compose UIのlintを確認する
みなさんJetpack Compose使ってますか。僕はあまり使っていないです。
そろそろ勉強しなければと手を付けているのですが、割とlintを触ることが多いので、その観点から
compose ui で公式に定義されているlintを紹介していきます。
lintなのでAndroid Studioで書いているときに気づけますが、全体として何が定義されているかを知ることで気をつけるポイントが予め分かるはずです。
Compose UIで定義されているlint
lintの定義自体はこちらから見ることが出来ます。
現在定義されているのは以下のISSUEになります。順番に見ていきましょう。
- UnnecessaryComposedModifier,
- ComposableModifierFactory,
- ModifierFactoryExtensionFunction,
- ModifierFactoryReturnType,
- ModifierFactoryUnreferencedReceiver,
- ModifierParameter,
- ExitAwaitPointerEventScope,
- MultipleAwaitPointerEventScopes
UnnecessaryComposedModifier
状態のあるModifierを作る時に使う composed
ですが、 composable functionを内部で呼ぶときだけ使うべきです。
呼ばない場合はわざわざ composed
を使う必要がないですし、 composedを使ったModifierはskippableでなくなり、パフォーマンスが悪化するためです。
// NG
fun Modifier.test(): Modifier = composed {
this.then(Modifier)
}
// OK
fun Modifier.test(): Modifier = this.then(Modifier)
ComposableModifierFactory
Modifier factoryには @Composable
をつけません。今ポーズ可能にしたい場合は composed
を使いましょう。
toplevelに置かれてcompositionの外でconstructさせるようにするため、と言っていますが、Composalbeの範囲なのでなるべく狭くしてコンポジションの回数を減らしたいということでしょうか。
// NG
@Composable
fun Modifier.fooModifier1(): Modifier {
val number by remember { mutableStateOf(0) }
return this.then(Modifier)
}
// OK
fun Modifier.fooModifier1() = composed {
val number by remember { mutableStateOf(0) }
this.then(Modifier)
}
ModifierFactoryExtensionFunction
Modifier fuctoryはModifierの拡張関数で定義すること。
基本的にModifierはチェーンでつなげて行って使うため、その作用にそうようにの意図だと思います。
// NG
fun fooModifier(modifier: Modifier): Modifier {
return modifier.then(TestModifier)
}
// OK
fun Modifier.fooModifier(): Modifier {
return this.then(TestModifier)
}
ModifierFactoryReturnType
Modifier factoryを返す時は子クラスではなく Modifier
自身を返すように。
基本的にComposeには Modifier
の型で渡しているので、子クラスで定義すると渡せなくなりますしね。
object TestModifier : Modifier.Element
// NG
fun Modifier.fooModifier(): Modifier.Element {
return this.then(TestModifier)
}
// OK
fun Modifier.fooModifier(): Modifier {
return this.then(TestModifier)
}
ModifierFactoryUnreferencedReceiver
Modifierがチェーンで渡ってきたら、ちゃんとそのModifierを使ってModifierを作れということです。
下のOKのように、 this
を直接使っていないとしても別関数( barModifier
)を呼ぶならlintは通ります。thisを使っていないとしたら barModifier
側でlintに引っかかるためです。
// NG
fun Modifier.fooModifier(): Modifier {
return HogeModifier
}
// OK
fun Modifier.fooModifier(): Modifier {
return this.then(HogeModifier)
}
// OK
fun Modifier.barModifier(): Modifier = this.then(BarModifier)
fun Modifier.fooModifier(): Modifier {
return barModifier()
}
ModifierParameterDetector
Modifier
に関するルールになります。
-
modifier
と名前をつける -
Modifier.element
などではなく、Modifier
型で宣言 - デフォルト値を持っていないか、 持っていたとしても
Modifier
- デフォルト引数付きのパラメータが複数あった場合、そのうち一番最初に宣言される
// NG
@Composable
fun ButtonWrongModifierName(
buttonModifier: Modifier
) {}
fun ButtonWrongModifierType(
modifier: Modifier.Element
) {}
fun ButtonDefaultModifier(
modifier: Modifier = HogeModifier
) {}
fun ButtonModifierLastParameter(
onClick: () -> Unit,
elevation: Float = 5f,
modifier: Modifier = HogeModifier
) {}
// OK
@Composable
fun Button(
onClick: () -> Unit,
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
elevation: Float = 5f,
) {}
ExitAwaitPointerEventScope
他のlintだと存在するドキュメントが存在しないため、もとのソースコードを張っておきます。
awaitPointerEventScope
はユーザーのタップイベントを待つスコープです。
PointerInputEventはこのスコープでキューイングされて処理されていきますが、例えば続けてこのブロックを宣言した場合、1つ目のスコープを抜けたタイミングで来たイベントが落ちることがあり、これがキューされなくなってしまいます。
ReturnFromAwaitPointerEventScope
awaitPointerEvent
の返り値をreturnしたり引数にとるのは禁止されています。スコープをでたことによってイベントが保証されなくなるためです。
公式ドキュメントにも awaitPointerEventScope
を使っているサンプルがありますが、このlintが引っかかってしまいます。
// NG
@Composable
fun MoveBoxWhereTapped() {
// Creates an `Animatable` to animate Offset and `remember` it.
val animatedOffset = remember {
Animatable(Offset(0f, 0f), Offset.VectorConverter)
}
Box(
// The pointerInput modifier takes a suspend block of code
Modifier.fillMaxSize().pointerInput(Unit) {
// Create a new CoroutineScope to be able to create new
// coroutines inside a suspend function
coroutineScope {
while (true) {
// Wait for the user to tap on the screen
val offset = awaitPointerEventScope {
awaitFirstDown().position
}
// Launch a new coroutine to asynchronously animate to where
// the user tapped on the screen
launch {
// Animate to the pressed position
animatedOffset.animateTo(offset)
}
}
}
}
)
}
// OK
@Composable
fun MoveBoxWhereTapped() {
// Creates an `Animatable` to animate Offset and `remember` it.
val animatedOffset = remember {
Animatable(Offset(0f, 0f), Offset.VectorConverter)
}
Box(
// The pointerInput modifier takes a suspend block of code
Modifier.fillMaxSize().pointerInput(Unit) {
// Create a new CoroutineScope to be able to create new
// coroutines inside a suspend function
coroutineScope {
while (true) {
awaitPointerEventScope {
val offset = awaitFirstDown().position
launch {
// Animate to the pressed position
animatedOffset.animateTo(offset)
}
}
}
}
}
)
}
おわりに
今回はcompose uiで定義されているlintについてまとめました。
パフォーマンスを悪化させる不要なComposableの宣言や、Modifierへのthisの利用し忘れを防ぐlintなどがあり、悪い書き方を制限してくれるのは特にCompose初心者の自分からありがたいですね。
Lintはquickfixを用意されてることも多いのでワンボタンで直せてしまいますが、根本原因を理解した上で行いたいですね。
Discussion