Twitter Jetpack Compose Rulesについて
今回は、DroidKaigiの公式アプリでも導入されていた、Twitter Jetpack Compose Rulesについてみていきたいと思います。
Twitter Jetpack Compose Rulesとは?
Twitter Jetpack Compose Rulesはktlintの一つで、Jetpack Composeのlinterです。🧑🔧
規模の大きなアプリにJetpack Composeを導入するのは大変なので、その手間を少しでも省くためにTwitterが開発したlinterとなっており、後で見ていきますが独自のルールが設定されています。
そのルールによって、細かくチームの中でJetpack Composeに関するルールを定めなくても自動でチェックしてくれるようになるのです。
Twitter Jetpack Composeを使うためには、ktlintかDetektと一緒に使う必要があります。
今回は、ルールを見た後に実際にktlintをとTwitter Jetpack Compose Rulesを使ってみたいと思います。
Twitter Jetpack Compose Rulesの詳細
ここからは、Twitter Jetpack Compose Rulesについて詳しく見ていきます。
以下が、実際のルールになっています。
Jetpack ComposeのStateに関するルール
引数に関するルール
Jetpack Composeは、unidirectional data flowの考え方に基づいており、単方向のデータの流れを意識して開発を進めていく必要があります。
実際にJetpack Composeで開発をしたことがある人なら、State Hoistingという考え方を元に、Stateレスな関数を作成することが重要であるということは理解していると思います。
その考え方を元に、Twitter Jetpack Compose Rulesでは以下のルールを定めています。
- ViewModelを子の関数に渡さない。
-
State<Some>
もしくはMutableState<Some>
のインスタンスを引数で渡さない
もし、ViewModelにあるメソッドを渡したい時や、State<Some>
の値を渡したいときはTwitter Jetpack Compose Rulesでは以下のようにして渡すようにする必要があります。
// 呼び出し側
.....
val someState = demoViewModel.someState.collectAsState()
.....
MyComposable(
someState = someState.value,
event = demoViewModel::someEvent
)
.....
@Composable
fun MyComposable(
someState: String,
event: () -> Unit,
) {
......
}
上のようにcollectAsState
を使用して、その値を渡すようにしたり、ViewModelのイベントを渡したい場合は関数参照やラムダを用いて、引数に渡すようにする必要があります。
実際のルール
rememberを使う
mutableStateOf
などを使って、Stateを作成するときはremember
を使っているかどうかチェックします。
remember
を使わないと無駄な再コンポーズが発生してしまうので、注意が必要です。
実際のルール
Immutableアノテーションを使用する
コンパイラに対して、immutableであると宣言するためにできるだけオブジェクトに対して@Immutable
アノテーションをつけると安全性が増すのでその部分をチェックします。
Kotlin collectionのList<T>
やMap<T>
などは、内部ではinterfaceとして定義されており、immutableであることが保証されないので、できるだけこれらを使わないようにするのが推奨されています。なので、この場合も@Immutable
アノテーションを使うことで、安全性を担保できるので、その部分もチェックしてくれます。
実際のルール
State HoistingなどJetpack ComposeのStateに関する内容は以下を参照して確認してください。
Composablesについて
Composable関数の役割について
Composable関数は、レイアウトを表示するか値を返すがどちらか一方の役割だけを担うべきであり、その両方の役割を果たしていないかどうかをチェックします。
実際のルール
Composable関数でレイアウトを表示する時について
Composable関数を新しく作成して、レイアウトを構成していくと思いますが、この時にComposable関数の内部で、2つ3つとレイアウトを出力していないかどうかをチェックしてくれます。
以下のような場合、チェックに引っかかります。
@Composable
fun Parent() {
Child(modifier = Modifier)
}
@Composable
fun Child(
modifier: Modifier = Modifier
) {
Text("Hello")
Text("World")
Button(.....)
}
この例では、Child
の内部で3つレイアウトを出力しています。
これではルールに引っかかってしまうので、以下のように改善する必要があります。
@Composable
fun Parent() {
Child(modifier = Modifier)
}
@Composable
fun Child(
modifier: Modifier = Modifier
) {
Colmun(
modifier = modifier
) {
Text("Hello")
Text("World")
Button(.....)
}
}
Jetpack Composeでは、従来のAndroid Viewよりもレイアウトのネストに関してパフォーマンスの低下が目立たないので、UIを正しく表示するためにもレイアウトを表示するComposable関数は1つのレイアウトを表示するようにする必要がありそうです。
ただし、以下のような場合は例外として許容されているようです。
@Composable
private fun ColumnScope.ChildContent() {
Text("Hello")
Text("World")
Button(.....)
}
この例では、ColumnScope
でレイアウトが定義されているのでチェックに引っかからず通すことができます。
実際のルール
Composition Local
Composition Local
を使用する際は、Local
を接頭辞としているかどうかをチェックします。
以下のような例です
// OK!
val LocalTheme = staticCompositionLocalOf<Theme>()
// Bad!
val ThemeLocal = staticCompositionLocalOf<Theme>()
実際のルール
Preview
複数のPreviewを使用する際に、カスタムアノテーションを作成した場合は、Previews
をつけているかどうかチェックします。
カスタムPreviewの例
@Preview(
name = "small",
group = "font scales",
fontScale = 0.5f
)
@Preview(
name = "medium",
group = "font scales",
fontScale = 1.0f
)
@Preview(
name = "large",
group = "font scales",
fontScale = 1.5f
)
annotation class FontScalePreviews
Previewのカスタムアノテーションを作成した場合は、今回のようなチェックがなされますが、シンプルに一つだけPreviewしたい場合は、Preview
だけでも問題ありません。
実際のルール
Composable関数の名前
レイアウトを表示するComposable関数(Unitを返す関数)に関しては、関数の名前は大文字で始める必要があり、逆に値を返すComposable関数は小文字で名前を始めているかどうかをチェックします。
実際のルール
Composable関数の引数の順番について
関数に渡す引数が、必須パラメーターを最初に書いて、オプショナルなパラメーターを必須パラメーターの後に書いているかどうかチェックしてくれます。
実際のルール
依存関係を分ける
Composable関数の内部で、ViewModelを取得するようにしていると、テスタビリティの観点でも悪影響が出てきますし、Composable関数を再利用しにくくなるので、ViewModelなどを取得したい場合は、デフォルト値としてInjectしているかどうかチェックしてくれます。
以下のような例です。
@Composable
fun DemoComposable(
val viewModel: DemoViewModel = viewModel()
) {
.......
}
このように引数のデフォルト値としてViewModelを指定する必要があります。
実際のルール
Previewがprivateかどうか
Previewでしか使用しない、Composable関数はpublicな状態にする必要がないので、privateになっているかどうかチェックしてくれます。
実際のルール
Modifierについて
Modifierを必ず渡す
Modifierを全ての関数に渡しているかどうかをチェックしてくれます。
なぜ、Modifierを渡す必要があるのかは以下の記事を見てみてください。
実際のルール
Modifierを使いまわさない
Modifierを渡したら、それを一番上の関数のModifierとしてだけ使っているかどうかチェックしてくれます。
Modifierを使い回すことで、意図しないレイアウトになることがあるので、ここも注意が必要です。
実際のルール
Modifierにデフォルト値を設定する
Modifierを必ず、引数に渡すようにする必要があると上で述べましたが、渡される側の関数ではModifierの引数に対してデフォルト値を指定しているかどうかをチェックしてくれます。
実際のルール
Modifierについて、詳しくみたい方は以下を見てみてください。
実際に使ってみる
まずは、ktlintを導入していきます。
plugins {
id("org.jmailen.kotlinter") version "3.12.0" apply false
}
plugins {
id("org.jmailen.kotlinter")
}
ここから下記を実行してみます。
$ ./gradlew formatKotlin
大量の指摘が出てきたので、ひとまず導入できました。
/Users/s18405/StudioProjects/AndroidDoughnut/app/src/main/java/com/github/ryutaro/androiddoughnut/domain/repository/ArticleRepository.kt:6:1: Format fixed > [no-unused-imports] Unused import
/Users/s18405/StudioProjects/AndroidDoughnut/app/src/main/java/com/github/ryutaro/androiddoughnut/domain/repository/ArticleRepository.kt: Format fixed
/Users/s18405/StudioProjects/AndroidDoughnut/app/src/main/java/com/github/ryutaro/androiddoughnut/domain/usecase/FetchAllArticleUseCaseImpl.kt:7:1: Format fixed > [no-unused-imports] Unused import
/Users/s18405/StudioProjects/AndroidDoughnut/app/src/main/java/com/github/ryutaro/androiddoughnut/domain/usecase/FetchAllArticleUseCaseImpl.kt: Format fixed
> Task :app:formatKotlinTest
/Users/s18405/StudioProjects/AndroidDoughnut/app/src/test/java/com/github/ryutaro/androiddoughnut/ui/viewmodel/HomeScreenViewModelTest.kt:8:1: Format fixed > [no-unused-imports] Unused import
/Users/s18405/StudioProjects/AndroidDoughnut/app/src/test/java/com/github/ryutaro/androiddoughnut/ui/viewmodel/HomeScreenViewModelTest.kt:23:5: Format fixed > [no-unused-imports] Unused import
/Users/s18405/StudioProjects/AndroidDoughnut/app/src/test/java/com/github/ryutaro/androiddoughnut/ui/viewmodel/HomeScreenViewModelTest.kt:27:7: Format fixed > [no-unused-imports] Unused import
/Users/s18405/StudioProjects/AndroidDoughnut/app/src/test/java/com/github/ryutaro/androiddoughnut/ui/viewmodel/HomeScreenViewModelTest.kt: Format fixed
次に、Twitter Jetpack Compose Rulesを導入してみます。
buildscript {
dependencies {
classpath "com.twitter.compose.rules:ktlint:0.0.18"
}
ここから下記を実行してみると
$ ./gradlew lintKotlin
無事実行できたようです🙌
最後に
今回は、Twitter Jetpack Compose Rulesについて見ていきました。
実際にチーム開発の中で、このルールを導入することによって、チーム内でJetpack Composeの秩序が保たれそうなので、積極的に導入して行ってもいいのではないかと思いました🙆♂️
参考
Discussion