jiko21’s techblog

フロントエンドエンジニア(React)からみたJetpack Compose

はじめに

同期に脅され薦められてJetpack Composeに触れたのでその所感をまとめてます。

ちなみに自分自身は

  • 学生時代にAndroidアプリ作るの教えてたし、2つほどアプリをリリースした
    • Kotlin + MVVM + RxJava(Kotlin?)でアプリ作ったことある(3年ほど前)
    • Javaの時代を知っている
    • FragmentのNavigationまでは知ってた
  • iOSも少し書いた(ほとんど忘れた)
    • Swiftもうほぼ書けない(忘れた)

だったので、モバイルアプリエンジニアではないです。

Jetpack Composeで遊んでみる

React自体の説明は公式のチュートリアルがあるのでそれを見てください。

ここではJetpack Composeを実際に使ってみます。今回遊びで作ったものは以下です。 テストケース

頑張ってMVVM + LiveDataとかもやってますがそこは省略します。

リポジトリはjiko21/TodoComposeです

Componentを実装する

まずは単純に与えられたデータを表示するComponentを実装してみましょう。 以下はタスクを表示するリストアイテムです。

リストアイテム カードのようなcontainer内にテキストとボタンがありますがこれをJetpack Composeで書くと次のようになります。

@Composable
fun ListItem(task: Task, onDelete: () -> Unit) {
    val padding = 16.dp
    Card(
        backgroundColor = colorResource(R.color.background),
        modifier = Modifier
            .border(
                width = 0.dp,
                color = Color.Transparent,
                shape = RoundedCornerShape(10.dp),
            )
            .fillMaxWidth()
    ) {
        Row(
            modifier = Modifier.padding(padding),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Text(text = task.content, fontSize = 24.sp)
            Button(
                onClick = onDelete,
                colors = ButtonDefaults.textButtonColors(
                    backgroundColor = colorResource(id = R.color.danger),
                    contentColor = Color.White,
                )
            ) {
                Text("Delete")
            }
        }
    }
}

ReactのFunction Componentと同様、引数を持ってますね。ここはpropsに対応します。 今回はTaskとイベントハンドラを引数に取ってます。 イベントハンドラに関しては、React同様、単純な関数を引数にしており、利用したい際も、

Button(onClick = onDelete)

のように指定するだけなのでほぼ一緒です。

また、paddingなる変数がありますが、これはReactの内部で普通に宣言される変数と同じようなものです。

では実際にViewを作る部分にフォーカスしてみましょう。

Card
├── Text
└── Button

というような階層構造になってますね。ここに関してはReactの描くDOM Treeと同じです。 これらの要素はJetpack Composeが標準で用意してくれているComposeです。

また、気になった方もいるかと思いますが、Buttonの中にTextがありますね。

Button(
    onClick = onDelete,
    colors = ButtonDefaults.textButtonColors(
        backgroundColor = colorResource(id = R.color.danger),
        contentColor = Color.White,
    )
) {
    Text("Delete")
}

Reactでいうchildrenみたいなものだと思ってください。

最後に、Kotlinに慣れていない人に向けて補足しておくと、Kotlinでは最後の式が評価され、戻り値となります。(これはrubyやRustに近いですね) なので、return文を付け足すと

@Composable
fun ListItem(task: Task, onDelete: () -> Unit) {
    val padding = 16.dp
    return Card(
        backgroundColor = colorResource(R.color.background),
        modifier = Modifier
            .border(
                width = 0.dp,
                color = Color.Transparent,
                shape = RoundedCornerShape(10.dp),
            )
            .fillMaxWidth()
    ) {
        Row(
            modifier = Modifier.padding(padding),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Text(text = task.content, fontSize = 24.sp)
            Button(
                onClick = onDelete,
                colors = ButtonDefaults.textButtonColors(
                    backgroundColor = colorResource(id = R.color.danger),
                    contentColor = Color.White,
                )
            ) {
                Text("Delete")
            }
        }
    }
}

これは実質Reactですね。

状態を定義する

とはいえ、このままだと単に与えられたpropsを表示するだけのFunctional Componentと変わりません。 ReactにはStateがありますがこれと同様のものがJetpack Composeにも存在します。

定義の仕方は簡単で次のように記述するだけです。

var isCreateModalOpen by remember {
    mutableStateOf(false)
}

これだけみると単方向のData bindingのように見えるかもしれませんが、 byデリゲートがあるため、getterやsetterが省略できているだけです。 これがなければ、useStateと同様に、代入不可能な値とDispatcherが返却されます。

以下は実際にStateを利用した例です。isCreateModalOpenという値でモーダルの開閉を制御しています。

@Composable
fun Container(viewModel: TaskViewModel) {

    var isCreateModalOpen by remember {
        mutableStateOf(false)
    }

    val lists = viewModel.tasks.observeAsState(arrayListOf())

    Scaffold(
        floatingActionButton = {
            FloatingActionButton(
                contentColor = Color.White,
                onClick = { isCreateModalOpen = true }) {
                Icon(Icons.Filled.Add, contentDescription = "add task")
            }
        },
        content = {
            Surface(
                modifier = Modifier.fillMaxSize(),
                color = MaterialTheme.colors.background
            ) {
                lists.value?.let {
                    TaskLists(lists = lists.value, onDelete = {id ->
                        viewModel.remove(id)
                    })
                }
                TaskCreateDialog(isCreateModalOpen,
                    onDismiss = { isCreateModalOpen = false },
                    onSubmit = { task ->
                        isCreateModalOpen = false
                        viewModel.insert(task)
                    },
                    onCancel = { isCreateModalOpen = false },
                )
            }
        }
    )
}

コンポーネントを確認しながら実装したい

フロントエンドエンジニアだと、実装しながらどのようなデザインか、を確認しながら実装したいかと思います。 こういったことも、Jetpack Composeで可能です。

例えば先ほど実装したListItemのデザインを確認したいなら、次のようなコードを付け足すだけで可能です。

@Preview(showBackground = true)
@Composable
fun PreviewListItem() {
    ListItem(task = Task(1, "服を洗う"), {})
}

Storybookとは違い、Android Studioだけで完結するのでかなりわかりやすいです。

preview

まとめ

関数ベースということでJetpack ComposeはReactに慣れている人だととっつきやすいですね。 もちろん、言語によってはトップレベルで関数を定義できないものもあったりするので、しょうがない部分はありますが、 宣言的UIを書くには関数の方が向いているなと改めて思いました。

Reactにあったこれがない、とかに関してはしゃぞーがないまだまだ勉強中な部分あるのでまた興味が湧けば書きます。

参考にしたもの

about author...

Frontend engineer.
loves: TypeScript, React, Node.js

more detail...