[compose multiplatform 中級チュートリアル3]アプリの見た目だけ作る

# 前回
[前回](https://qiita.com/shunk0616/private/7906f35e7093cc004805)はページネーションでした。

# はじめに
作ろうとしているのはリスト画面の見た目です。
イメージとしてはこんな感じにしてみようと思います。（アナログ人間なので手書きです）
![](https://storage.googleapis.com/zenn-user-upload/1a704174f65f-20231109.jpg)

タスクを三つのカテゴリに分類して、「残っているタスク」「実行中」「完了」に振り分けます。
新しいタスクの作成時にはどのくらい時間がかかるかを入力して、タスクが完了した時には、予想した時間と実際の時間がどのくらいずれているのかを統計してくれるようなアプリを作ります。

また、できたらタスクはドラッグ&ドロップで移動できるようにしたいですね。

伝わったかな。。。？
まぁ作り始めましょう

# データ型だけ定義する
ListScreen.ktの中でimportの下あたりに以下のデータクラスを作っておきます。
```
data class Task(val title: String, val estimatedTime: Int, val category: String)
```
あとで必要になるのですが、一旦は書いておけば良いです。

# 各部品を作る
今回必要なのはカテゴリとタスク、入力フォームの三つのコンポーザブルです。
本当はもっと粒度を揃えた方が良いのでしょうが、めんどくさいのでここでは分けません。

*commonMain/kotlin/アプリ名*のディレクトリにCommon.Composableフォルダを作りましょう

そのフォルダの中に三つのファイルを作ります。
ここではCategory.kt, ListItem.kt, TaskForm.ktとします。

## コンポーザブルの中身をかく
細かい解説はコード上のコメントアウトに書きました。
ListItem.kt
```
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.company.saikyotodo.List.Task

@Composable
fun ListItem(task: Task) {
    Text("- ${task.title} (${task.estimatedTime}h)", modifier = Modifier.padding(4.dp))
}
```

Category.kt
```
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.company.saikyotodo.List.Task

// カテゴリ名と全タスクを渡す（ほんとは絞り込んだ後の方がパフォーマンスが良いが目を瞑ろう）
@Composable
fun Category(category: String, tasks: List<Task>) {
    // カテゴリ名を表示
    Text(text = category, style = TextStyle(fontSize = 20.sp), modifier = Modifier.padding(4.dp))
    // カテゴリ名が一致するタスクをListItemコンポーザブルを使って表示
    tasks.filter { it.category == category }.forEach { task ->
        ListItem(task)
    }

    // 適当な余白
    Spacer(modifier = Modifier.height(20.dp))
}
```

TaskForm.kt
```
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.company.saikyotodo.List.Task

@Composable
fun TaskForm(
    onTaskAdded: (Task) -> Unit,
    categories: List<String>
) {
    // 最初にrememberを使ってステートを定義
    var newTitle by remember { mutableStateOf("") }
    var newEstimatedTime by remember { mutableStateOf(1) }
    var newCategory by remember { mutableStateOf(categories.firstOrNull() ?: "") }
    Text("タスク追加", style = TextStyle(fontSize = 16.sp))
    // タイトル入力フィールド
    TextField(
        value = newTitle,
        onValueChange = { newTitle = it },
        placeholder = { Text("タスク名") },
        singleLine = true,
        modifier = Modifier.fillMaxWidth(),
        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
    )
    // 予定工数選択
    Row(verticalAlignment = Alignment.CenterVertically) {
        Button(onClick = { if(newEstimatedTime > 0) newEstimatedTime-- }) {
            Text("-")
        }
        Text(newEstimatedTime.toString(), Modifier.widthIn(min = 20.dp))
        Button(onClick = { newEstimatedTime++ }) {
            Text("+")
        }
    }
    // 保存ボタン
    Button(
        onClick = {
            // 新しいタスクをリストに追加
            onTaskAdded(Task(newTitle, newEstimatedTime, newCategory))
            // 保存したらフォームはリセット
            newTitle = ""
            newEstimatedTime = 1
            newCategory = categories.firstOrNull() ?: ""
        }
    ) {
        Text("保存")
    }
}
```

これで部品は完成

# 部品を並べる
先ほど作った部品(composable)をListScreen.ktの中で使っていきましょう

```
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.company.saikyotodo.Common.Composable.Category
import org.company.saikyotodo.Common.Composable.TaskForm

// タスクデータクラスの定義
data class Task(val title: String, val estimatedTime: Int, val category: String)

class ListScreen(): Screen {
    @Composable
    override fun Content() {
        val viewModel = rememberScreenModel { ListScreenModel() }
        var tasks by remember { mutableStateOf(listOf(
            Task("サンプルタスク1", 1, "バックログ"),
            Task("サンプルタスク2", 2, "対応中")
        ))}
        val categories = listOf("バックログ", "対応中", "完了")

        Column(modifier = Modifier.padding(20.dp).fillMaxSize()) {
            // カテゴリごとにCategoryコンポーザブルを呼び出す
            categories.forEach { category ->
                Category(category, tasks)
            }


            // タスクの追加セクション
            TaskForm(
                onTaskAdded = { task ->
                    tasks = tasks + task
                },
                categories = categories
            )
        }
    }
}
```

これで簡単なtodoリストの見た目は完成です。
見た目が悪いのは気にしないでください。
後の章で直します。

色々触ってみてください。データは保存されていないですが、タスクを追加したらバックログに新しいタスクが増えていくのは確認していただけると思います。

# 次
[次回](https://qiita.com/shunk0616/private/a61be6e397534ca393e8)はバックエンドを検討してみます。