Android Jetpack Compose(Beta)をざっくり理解するPathwayメモ

10 min read読了の目安(約9700字

背景

  • Jetpack ComposeのDev Challengeが開催されてます。
  • 景品が欲しかったので挑戦してみる
    • pixel5も欲しいけども厳しそう。。
  • いったん今週のは出せたので、メモも兼ねて記録しておく。

内容

Jetpack ComposeのPathway

Pathwayはこちら。

https://developer.android.com/courses/pathways/compose

概要

1. Composeとは

  • 今まではXMLを定義して状態を連携するような形だった
    • これらはビューと状態の紐付けを忘れがちだったり、予期しないタイミングでの更新が走ったりとメンテナンス性を下げていた。
  • そのためこれからは宣言的UIヘ移行していくよ。
    • Composeのいいところは状態がない。途中でデータを入れ替えて挙動を変えることがない。immutableであるところ
    • もし状態が変わった時、Compose関数を再度呼び出すことでUIは新しいデータで再度作り直される(再コンポーズ)
    • UIツリー全体を再コンポーズするとコストが大きい。Composeでは新しい入力に基づいて再コンポーズされると、変更された可能性のある関数またはラムダのみを呼び出し、残りはスキップします。インテリジェンスですね。
    • なので再コンポーズはスキップされる可能性があるものと認識し、コンポーズ可能な関数の実行による副作用に依存しちゃダメ。
    • 例えば以下のようなアクションは全て危険な副作用になるよ。冪等性を保って行こうね。
      • コンポーズ内で共有オブジェクトに書き込む
      • コンポーズ内で監視してデータ更新
      • コンポーズ内で共有設定の更新
    • コンポーズ可能な関数に関しては任意の順番で呼び出されるので、順番に依存する組み方はダメだよ。
    • あと何回も呼び出される可能性があるからね。副作用ダメだよ。

2. Composeの基本的な使い方

最初のセットアップはここを参照。

https://developer.android.com/jetpack/compose/setup
  • Android StudioはCanaryを使うこと(2021/2/26時点)
  • build.gradleの設定は書かれているとおりに設定すること

あとはNew->NewProject -> Empty Compose Activity
を選ぶとComposeを使う上でのbuild.gradleが大体整っているのでこちらを使おう。

基本的にはsetContext{}の中にCompose関数を設定することでUIを宣言的に作れる。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text("Hello world!")
        }
    }
}

@Composableアノテーションをメソッドにつけることで、コンポーズ可能な関数として扱うことができる。

@Composable
fun Greeting(name: String) {
    Text (text = "Hello $name!")
}

@ComposableメソッドでUIを宣言して、データに応じてこの@Composableメソッドを呼んでいくのが基本形となる。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Greeting("Android")
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text (text = "Hello $name!")
}

3. Composeのコンポーネント

よく出てくるのは以下のコンポーネント群。

Surface

要素の背景色を設定できる。

        setContent {
            ComposeSampleTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.primary) {
                    Greeting("Android")
                }
            }
        }

こうすると背景色はこうなる。

colors.backgroundにするとこう。

        setContent {
            ComposeSampleTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    Greeting("Android")
                }
            }
        }

自動で文字色まで変わっている。これはprimary設定をしたため、ThemeクラスのonPrimaryが適用されたよう。ここら辺の設定をいじりたかったらThemeクラスを変更していく必要がある。

こんなのがある。

fun lightColors(
    primary: Color = Color(0xFF6200EE),
    primaryVariant: Color = Color(0xFF3700B3),
    secondary: Color = Color(0xFF03DAC6),
    secondaryVariant: Color = Color(0xFF018786),
    background: Color = Color.White,
    surface: Color = Color.White,
    error: Color = Color(0xFFB00020),
    onPrimary: Color = Color.White,
    onSecondary: Color = Color.Black,
    onBackground: Color = Color.Black,
    onSurface: Color = Color.Black,
    onError: Color = Color.White
): Colors

Modifiers

UIの要素にPaddingなどの表示を指示できる。

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!", modifier = Modifier.padding(24.dp))
}

指定するとこんな感じ。

他にも

  1. 横幅いっぱい表示するfillMaxWidth() (fillMaxHeightもある)
@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!", modifier = Modifier.padding(24.dp).fillMaxWidth())
}

  1. クリックイベントを設定するclicable()
@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!", modifier = Modifier.padding(24.dp).fillMaxWidth().clickable {
        /* TODO */
    })
}
  1. スクロール可能かを設定するScrollable
@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!", modifier = Modifier
        .padding(24.dp)
        .fillMaxWidth()
        .scrollable(
            rememberScrollState(),
            orientation = Orientation.Horizontal
        ) 
        .clickable {
            /* TODO */
        })
}
  1. alignを整える
 Column(
            modifier = Modifier
                .padding(start = 8.dp)
                .align(Alignment.CenterVertically)
        ) 
  1. clipする
    Row(modifier
        .padding(8.dp)
        .clip(RoundedCornerShape(4.dp)) // 4dp角丸
        .background(MaterialTheme.colors.surface)
        .clickable(onClick = { /* Ignoring onClick */ })
        .padding(16.dp)
    ) {
     

などがある。よく出てきそう。

Column (Row)

この要素は縦に積むよ、という宣言。Linerlayoutのorientation = verticalと同じ雰囲気。なおRowはhorizontal。

Surface(color = MaterialTheme.colors.primary) {
    Column {
        Greeting("Android")
        Divider(color = Color.Black)
        Greeting("there")
    }
}

Divider

分割線を引くやつ。Color指定ができる。

animateColorAsState

状態に応じた色変更アニメーションをつけられる

@Composable
fun Greeting(name: String) {

    var isSelected by remember { mutableStateOf(false) }
    val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)

    Text(text = "Hello $name!", modifier = Modifier
        .padding(24.dp)
        .fillMaxWidth()
        .scrollable(
            rememberScrollState(),
            orientation = Orientation.Horizontal
        )
        .background(backgroundColor)
        .clickable(onClick = { isSelected = !isSelected }))
}

Box

コンポーネントを重ね合わせたいときに使う。

Shape

おなじみShape。コンポーネントの形を変えたい時に使う

@Composable
fun PhotographerCard() {
    Row {
        Surface(
            modifier = Modifier.size(50.dp),
            shape = CircleShape,
            color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
        ) {
            // Image goes here
        }
        Column {
            Text("Alfred Sisley", fontWeight = FontWeight.Bold)
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("3 minutes ago", style = MaterialTheme.typography.body2)
            }
        }
    }
}

CircleShapeはRoundedCornerShape(58)の定義。ちょっと角丸にしたいとかであればRoundedCornerShape(8.dp)あたりを入れておけば良さそう。

Scaffold

足場という意味らしい。これをつけておくとマテリアルデザインが適用される。

    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text(text = "LayoutsCodelab")
                },
                actions = {
                    IconButton(onClick = { /* doSomething() */ }) {
                        Icon(Icons.Filled.Favorite, contentDescription = null)
                    }
                }
            )
        }
    ) { innerPadding ->
        BodyContent(Modifier.padding(innerPadding))
    }

topBar

Toolbarっぽいやつ。titleとactionをつけられる。

Composeの状態について

いくらステートレスとはいえど、今クリックしたかどうかについては持っていたいこともありそう。
そういう時はmutableStateOfとremember使用することで再コンポーズされても状態を保持する。

    var isSelected by remember { mutableStateOf(false) }
    val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)

また、アプリがキルされても復元したい場合は、rememberSaveableを用いる

    var isSelected by rememberSaveable { mutableStateOf(false) }
    val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)

ステートレスコンポーザブル

こういった状態をコンテンツが保持していると、再利用とテストがしづらいのでステートレスコンポーザブルにする必要がある。

@Composable
fun screen() {
  var isSelected by rememberSaveable { mutableStateOf(false) } 
  content(isSelected = isSelected)
}

@Composable
fun Content(isSelected : Boolean) {
  val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)
}

状態は以下の3つの方法で宣言できる。

val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }

Desugerでとる(value, setValue)は以下を指しているよ。

value = 現在の値
setValue = 今から代入する値

内部状態をUIコンポーザブルから分離

例えば最初からチェックボックスに値を入れておきたいという要望が発生するかもしれない。ただそれは使う人によって異なるかも。そのような場合はオーバーロードでコンポーザブルを定義しておいて、ステートレスな関数とステートフルな関数に分けよう。

プロセスの再生成後にUIの状態を復元する

ComposeのLayoutについて注意すること

  • Modifierは順序が大事。
    • clickableしてからpaddingなのか、paddingしてからclicableなのかでクリック可能な範囲が異なるよ
  • SlotsAPIを使えば、コンポーネントのパターン化ができるよ。
    • おんなじButtonで中身だけ変えたいときはよくあるよね
    • その度に定義してたらどこかで失敗するよね
    • なのでテンプレを作ることができるよ
  • Scaffoldを使うと基本的なマテリアルデザインのレイアウト構造でUIを実装できるよ。
    • ToolBar的なTopAppBarもあるよ。actionも追加できるよ。
  • Scrollについては簡単に実施するならColumにスクロール可能な設定をつける。
    • ただしこれは画面上見えない部分までレンダリングするのでよくないよ。
    • この問題を回避するならLazyColumnを使おうね。
    • scrollState.animateScrollToItem()で指定位置までスクロールできるよ。
  • アプリ固有のレイアウトを作りたいときはカスタムレイアウトを作れるよ。
  • ConstraintLayoutも作れるよ。
  • Composeのルールの一つとして、2回以上子の計測をしない、というものがあるよ。
    • 予め子の計測が欲しかったらmodifier.preferredHeight()を使って計測しよね。

まとめ(感想)

  • かなりいい感じ。使いやすい。
  • ただプレビューがめちゃくちゃ遅いので今は普通に実行したほうが早いかもしれない。。
  • ComposeにもNavigationがあり、こちらを使うとFragmentが必要なさそうなのが何より。

追記

  • まだPathway半分くらいしかできてないのでもうちょっと進めたら追記します。