こんにちは、カンカクでAndroidエンジニアをやっている @haru067です。
最近はJetpack Compose(以下、Compose)をゴリゴリに書いています。
ということで、今回はComposeの話をしたいと思います。
(執筆時のComposeのstableバージョンは1.0.5であり、これを想定して説明します。)
TextFieldとエラーラベル
Composeには文字入力を扱うComposableとして、TextField
があります。
@Composable fun Example() { TextField( value = "value", onValueChange = {}, label = { Text("Error label") }, isError = true, ) }
label
を設定すると、入力文字の上にラベルが表示されます。isError = true
を設定したときにはエラー表示となり、ラベルは赤色で表示されます。
ここでクイズ
次の2つのComposable、Quiz1
とQuiz2
を実行したとき、エラーラベルの色はそれぞれ何色になるでしょう?
@Composable fun Quiz1() { MaterialTheme(typography = Typography(body1 = TextStyle(Color.Green))) { TextField( value = "value", onValueChange = {}, label = { Text("Error label1", color = Color.Blue, style = TextStyle(Color.Yellow)) }, isError = true, ) } } @Composable fun Quiz2() { MaterialTheme(typography = Typography(body1 = TextStyle(Color.Green))) { TextField( value = "value", onValueChange = {}, label = { Text("Error label2") }, isError = true, ) } }
1. Color.Green 2. Color.Blue 3. Color.Yellow 4. エラーの赤色 5. それ以外
正解
次のリンクを押して、答えを確認してみてください。
いかがでしたでしょうか? 両方とも正解できた方はおめでとうございます! このあとの文章を読む必要はなさそうなので、こちらをクリックしてください。
正解できなかった人も安心してください。このあとしっかり解説します。
解説: Quiz1
Quiz1
はそこまで難しくありません。Text
の実装を確認すると、Text
は次のようなロジックで文字色を決定していることがわかります。
- 引数
color: Color
が指定してある場合、color
を使用する - 引数
style: TextStyle
のstyle.color
が指定してある場合、style.color
を使用する - それ以外の場合は
LocalContentColor
を参照する(後述)
Quiz1
はText("Error label1", color = Color.Blue, ...)
となっており、color
が最優先された結果、エラーラベルはColor.Blue
になります。
ということで、これは単純な優先順位の問題でした。厄介なのはQuiz2
です。Quiz2
では、Text
に何も指定していません。単純に考えるとエラーの赤色が適用されそうですが、実際にはそうなりませんでした。謎ですね。
これを理解するには、CompositionLocal
について知る必要があります。
CompositionLocal
CompositionLocal
は、Composableに対して暗黙的にデータ渡すための仕組みです。
・・・と言っても理解しづらいと思うので、まずは例を見てみましょう。
@Composable fun CompositionLocalExample() { MaterialTheme { Column { // MaterialThemeがデフォルト値としてContentAlpha.highを設定 Text("High alpha") CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { Text("Medium alpha") CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) { DescendantExample() } } } } } @Composable fun DescendantExample() { // Composableを跨ぐ場合も適用される Text("Disabled alpha") }
Text
に何も指定していないにも関わらず、alpha値が変化しています。
Text
は内部でLocalContentAlpha.current
という値を参照しており、CompositionLocalProvider
を通じてLocalContentAlpha
に値を設定することでalpha値が暗黙的に変化する、という仕組みになっています。
なんて危険な仕組みだ!と思った人は嗅覚が鋭いです。知らないうちに値が変わるということは便利であると同時に、問題が発生したときのデバッグを困難にします。実際、公式ドキュメントでも使うべき/使うべきでない用途については丁寧に解説されています。 developer.android.com
では逆に、これを使うべきタイミングはいつでしょうか?典型的な例はThemingです。先程の例ではMaterialTheme
で囲った箇所のalpha値が変化していますが、MaterialTheme
の実装を覗くと、CompositionLocalProvider
を使用していることがわかります。
脱線: provides
LocalContentAlpha provides ContentAlpha.medium
という表記を見て「ん?」と思った方は多いのではないでしょうか。
provides
は中置記法で、ComposeというよりはKotlinの機能です。LocalContentAlpha provides ContentAlpha.medium
とLocalContentAlpha.provides(Content.Alpha.medium)
と実質的に同じです。
解説: Quiz2
さて、CompositionLocal
について学んだところで、Quiz2
に戻りましょう。
@Composable fun Quiz2() { MaterialTheme(typography = Typography(body1 = TextStyle(Color.Green))) { TextField( value = "value", onValueChange = {}, label = { Text("Error label2") }, isError = true, ) } }
Quiz2
ではMaterialTheme
でtypography
を指定していました。
実装を確認すると、MaterialTheme
ではtypography.body1に対して
CompositionLocalProvider
でLocalTextStyleを設定していることがわかります。
ここで
Text
の文字色の決定ロジックを再確認しましょう。
- 引数
color: Color
が指定してある場合、color
を使用する - 引数
style: TextStyle
のstyle.color
が指定してある場合、style.color
を使用する - それ以外の場合は
LocalContentColor
を参照する
ポイントは2です。style
のデフォルト値は
LocalTextStyle.currentなので、Quiz2
におけるText
のstyle
の値はMaterialTheme(...)
でprovideされたスタイルになります。
この結果、style.color
はMaterialTheme.typography.body1.color
ということになり、Color.Green
が優先された、となるわけです。
エラー時の振る舞い
残る最後の疑問は「エラー時の赤色はどうやって実現しているのか?」です。
余白が少ないので細かい流れは追いませんが、TextFieldの実装を追っていくと、最終的にはここにたどり着きます。要するに、TextField
のlabel
はLocalContentColor
をprovideしており、label = {...}
内に記述されたText
は
3.それ以外の場合は
LocalContentColor
を参照する
の規則に則って赤色になるわけです。おしまい。
おわりに
カンカクでは、Jetpack ComposeやCompositionLocalに興味のあるAndroidエンジニアを募集しています。 気になる方は以下のリンクよりご気軽にご応募ください!