タスクを編集できるようにする

タスクを編集できるようにするには、以下のことを行います。

  1. 編集モードの管理
    どのタスクが現在編集モードにあるかを管理するための状態を導入します。
  2. 編集用の TextField を表示する
    タスクが編集モードの場合、そのタスクの表示テキストを TextField に置き換えます。
  3. 編集を保存するボタン(またはアイコン)を追加する
    編集モードの TextField の横に、変更を確定するためのボタン(またはアイコン)を追加します。
  4. 編集をキャンセルするボタン(またはアイコン)を追加する
    編集モードを終了し、変更を破棄するためのボタン(またはアイコン)を追加します。

具体的な手順

TodoListScreen 関数を編集していきます。

1. 必要なimportを追加

ファイルの先頭あたりに、以下を追加してください。(既にあるものもあるかもしれません)

import androidx.compose.material.icons.filled.Edit // 編集アイコン用
import androidx.compose.material.icons.filled.Done // 完了アイコン用
import androidx.compose.material.icons.filled.Close // キャンセルアイコン用
import androidx.compose.material3.BasicTextField // 編集モードで使うシンプルなTextField

2. TodoListScreen を修正して編集機能を実装する

TodoListScreen 関数の中の LazyColumn の items ブロックを中心に変更します。

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TodoListScreen(navController: NavController) {
    var todoItems by rememberSaveable { mutableStateOf(mutableListOf<TodoItem>()) }
    var newTodoText by rememberSaveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }

    var expanded by remember { mutableStateOf(false) }
    var selectedCategory by remember { mutableStateOf(TodoCategory.NONE) }

    // 現在編集中のタスクのIDを保持する状態変数 (nullの場合は編集中のタスクなし)
    var editingItemId by remember { mutableStateOf<Long?>(null) }
    // 編集中のタスクのテキストを保持する状態変数
    var editingText by rememberSaveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
    // 編集中のタスクのカテゴリを保持する状態変数
    var editingCategory by remember { mutableStateOf(TodoCategory.NONE) }
    // 編集中のカテゴリ選択ドロップダウンの状態
    var editingCategoryExpanded by remember { mutableStateOf(false) }


    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "ToDoリスト",
            fontSize = 24.sp,
            modifier = Modifier.padding(bottom = 16.dp)
        )

        OutlinedTextField(
            value = newTodoText,
            onValueChange = { newValue -> newTodoText = newValue },
            label = { Text("新しいToDo") },
            singleLine = true,
            modifier = Modifier
                .fillMaxWidth()
                .padding(bottom = 8.dp)
        )

        ExposedDropdownMenuBox(
            expanded = expanded,
            onExpandedChange = { expanded = !expanded },
            modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
        ) {
            OutlinedTextField(
                value = selectedCategory.displayName,
                onValueChange = {},
                readOnly = true,
                label = { Text("カテゴリを選択") },
                trailingIcon = { Icon(Icons.Filled.ArrowDropDown, "ドロップダウン") },
                modifier = Modifier.menuAnchor().fillMaxWidth()
            )
            ExposedDropdownMenu(
                expanded = expanded,
                onDismissRequest = { expanded = false }
            ) {
                TodoCategory.values().forEach { category ->
                    DropdownMenuItem(
                        text = { Text(category.displayName) },
                        onClick = {
                            selectedCategory = category
                            expanded = false
                        }
                    )
                }
            }
        }

        Button(
            onClick = {
                if (newTodoText.text.isNotBlank()) {
                    val newTodo = TodoItem(
                        text = newTodoText.text,
                        category = selectedCategory
                    )
                    todoItems = (todoItems + newTodo).toMutableList()
                    newTodoText = TextFieldValue("")
                    selectedCategory = TodoCategory.NONE
                }
            },
            modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)
        ) {
            Text("追加")
        }

        LazyColumn(
            modifier = Modifier.fillMaxSize()
        ) {
            items(todoItems, key = { it.id }) { item ->
                Card(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 4.dp),
                    border = BorderStroke(1.dp, Color.LightGray)
                ) {
                    // 編集中のアイテムかどうかで表示を切り替える
                    if (editingItemId == item.id) {
                        // 編集モードの場合
                        Row(
                            modifier = Modifier
                                .fillMaxWidth()
                                .background(editingCategory.color) // 編集中のカテゴリの色を反映
                                .padding(horizontal = 16.dp, vertical = 8.dp),
                            verticalAlignment = Alignment.CenterVertically,
                            horizontalArrangement = Arrangement.SpaceBetween
                        ) {
                            // 編集用TextField
                            BasicTextField( // BasicTextFieldは枠線がないシンプルなTextField
                                value = editingText,
                                onValueChange = { editingText = it },
                                singleLine = true,
                                modifier = Modifier
                                    .weight(1f)
                                    .padding(end = 8.dp)
                            )
                            // 編集用カテゴリ選択ドロップダウン
                            ExposedDropdownMenuBox(
                                expanded = editingCategoryExpanded,
                                onExpandedChange = { editingCategoryExpanded = !editingCategoryExpanded }
                            ) {
                                Text(
                                    text = editingCategory.displayName,
                                    modifier = Modifier
                                        .menuAnchor()
                                        .padding(horizontal = 4.dp)
                                        .background(Color.LightGray) // 選択中のカテゴリを少し目立たせる
                                        .clickable { editingCategoryExpanded = true }
                                )
                                ExposedDropdownMenu(
                                    expanded = editingCategoryExpanded,
                                    onDismissRequest = { editingCategoryExpanded = false }
                                ) {
                                    TodoCategory.values().forEach { category ->
                                        DropdownMenuItem(
                                            text = { Text(category.displayName) },
                                            onClick = {
                                                editingCategory = category
                                                editingCategoryExpanded = false
                                            }
                                        )
                                    }
                                }
                            }
                            // 編集完了ボタン
                            IconButton(onClick = {
                                todoItems = todoItems.map { todo ->
                                    if (todo.id == item.id) {
                                        todo.copy(text = editingText.text, category = editingCategory)
                                    } else {
                                        todo
                                    }
                                }.toMutableList()
                                editingItemId = null // 編集モード終了
                            }) {
                                Icon(Icons.Filled.Done, contentDescription = "編集を保存")
                            }
                            // 編集キャンセルボタン
                            IconButton(onClick = {
                                editingItemId = null // 編集モード終了
                            }) {
                                Icon(Icons.Filled.Close, contentDescription = "編集をキャンセル")
                            }
                        }
                    } else {
                        // 通常表示モードの場合
                        Row(
                            modifier = Modifier
                                .fillMaxWidth()
                                .background(item.category.color)
                                .padding(horizontal = 16.dp, vertical = 8.dp),
                            verticalAlignment = Alignment.CenterVertically,
                            horizontalArrangement = Arrangement.SpaceBetween
                        ) {
                            Text(
                                text = "[${item.category.displayName}] ${item.text}",
                                modifier = Modifier
                                    .weight(1f)
                                    .padding(end = 8.dp)
                            )
                            // 編集ボタン
                            IconButton(onClick = {
                                editingItemId = item.id // 編集モード開始
                                editingText = TextFieldValue(item.text) // 編集テキストを初期化
                                editingCategory = item.category // 編集カテゴリを初期化
                            }) {
                                Icon(Icons.Filled.Edit, contentDescription = "編集")
                            }
                            // 削除ボタン
                            IconButton(onClick = {
                                todoItems = todoItems.filter { it.id != item.id }.toMutableList()
                            }) {
                                Icon(Icons.Filled.Delete, contentDescription = "削除")
                            }
                        }
                    }
                }
            }
        }

        Button(
            onClick = { navController.popBackStack() },
            modifier = Modifier.padding(top = 16.dp)
        ) {
            Text("戻る")
        }
    }
}
  • editingItemId: 現在編集中の TodoItem の id を保持します。null の場合、どの項目も編集されていません。
  • editingText と editingCategory: 編集中の TextField の値とカテゴリ選択の状態を保持します。
  • if (editingItemId == item.id) { … } else { … }: 各 TodoItem が現在編集中のものかどうかを editingItemId と比較して、表示するUIを切り替えます。
  • 編集モードのUI:
    • BasicTextField: 背景や枠線がないシンプルなテキスト入力欄です。
    • ExposedDropdownMenuBox を使って、編集モードでもカテゴリを変更できるようにしました。
    • IconButton(onClick = { Icons.Filled.Done … }): 編集内容を保存するアイコンボタンです。todoItems リストを map 関数で変更し、該当する id の項目だけ copy で新しい text と category に更新します。
    • IconButton(onClick = { Icons.Filled.Close … }): 編集をキャンセルするアイコンボタンです。editingItemId = null にするだけで編集モードを終了します(変更は破棄されます)。
  • 通常表示モードのUI:
    • IconButton(onClick = { Icons.Filled.Edit … }): 編集モードを開始するためのアイコンボタンです。editingItemId を現在の項目の id に設定し、editingText と editingCategory を現在の項目の値で初期化します。

これで、ToDoリストの各項目に編集ボタンと削除ボタンが表示され、編集ボタンを押すとテキストとカテゴリが編集可能になり、保存・キャンセルができるようになります!

目次に戻る