Mokelab Blog

#74 Jetpack ComposeでToDoアプリを作る - ToDoの編集機能

ToDoの詳細表示を実装したので、次は編集機能を実装してみます。

TopAppBarにアクションを追加する

編集画面への遷移は、詳細画面の右上に鉛筆アイコンを配置し、そこをタップした際に遷移するようにしてみます。

@Composable
fun DetailTopBar(
  navController: NavController,
  todo: ToDo,
  toEdit: () -> Unit,
) {
  TopAppBar(
    navigationIcon = {
      IconButton(onClick = {
        navController.popBackStack()
      }) {
        Icon(Icons.Filled.ArrowBack, "Back")
      }
    },
    title = {
      if (todo._id == emptyToDoId) {
        Text(stringResource(id = R.string.loading))
      } else {
        Text(todo.title)
      }
    },
    actions = {
      if (todo._id != emptyToDoId) {
        // 編集アイコン
        IconButton(onClick = toEdit) {
            Icon(Icons.Filled.Edit, "Edit")
        }
      }
    }
  )
}

TopAppBarにアクションアイコンを追加するには、actionsパラメータでIconButtonを使います。

ToDo編集画面のレイアウトを作る

レイアウト自体は作成画面と同じものでよいでしょう。

@Composable
fun EditToDoBody(
  title: MutableState<String>,
  detail: MutableState<String>,
) {
  Column {
    TextField(
      value = title.value,
      onValueChange = { title.value = it },
      label = { Text(stringResource(id = R.string.todo_title)) },
      singleLine = true,
      modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 16.dp, vertical = 8.dp)
    )
    TextField(
      value = detail.value,
      onValueChange = { detail.value = it },
      label = { Text(stringResource(id = R.string.todo_detail)) },
      modifier = Modifier
        .weight(1.0f, true)
        .fillMaxWidth()
        .padding(horizontal = 16.dp, vertical = 8.dp)
    )
  }
}

ルート部分は、編集対象のToDoの読み込みが完了しているかどうかで分岐させます。

@Composable
fun EditToDoScreen(
  navController: NavController,
  viewModel: EditToDoViewModel,
) {
  val scaffoldState = rememberScaffoldState()
  val todo = viewModel.todo.collectAsState(emptyToDo)
  // 読み込み中を表示
  if (todo.value._id == emptyToDoId) {
    Scaffold(
      scaffoldState = scaffoldState,
      topBar = {
          EditTopBar(navController, null)
      },
    ) {
      CircularProgressIndicator()
    }
    return
  }
  val title = rememberSaveable { mutableStateOf(todo.value.title) }
  val detail = rememberSaveable { mutableStateOf(todo.value.detail) }

  Scaffold(
    scaffoldState = scaffoldState,
    topBar = {
      EditTopBar(navController) {
        viewModel.save(todo.value, title.value, detail.value)
      }
    },
  ) {
    EditToDoBody(title, detail)
  }
}

Topbar部分も作成画面と同じようにしました。読み込み中に編集完了ボタンを押されると困るので、savenullかどうかで分岐させています。

@Composable
fun EditTopBar(navController: NavController, save: (() -> Unit)?) {
  TopAppBar(
    navigationIcon = {
      IconButton(onClick = {
        navController.popBackStack()
      }) {
        Icon(Icons.Filled.ArrowBack, "Back")
      }
    },
    title = {
      Text(stringResource(id = R.string.edit_todo))
    },
    actions = {
      if (save != null) {
        IconButton(onClick = save) {
          Icon(Icons.Filled.Done, "Save")
        }
      }
    }
  )
}

更新処理を実装する

EditToDoViewModelsave()を実装していきます。といっても作成とほぼ同じような処理ですが。。。

@HiltViewModel
class EditToDoViewModel @Inject constructor(
  private val repo: ToDoRepository
) : ViewModel() {
  private val todoId = MutableStateFlow(-1)

  @ExperimentalCoroutinesApi
  val todo: Flow<ToDo> = todoId.flatMapLatest { todoId -> repo.getById(todoId) }

  val errorMessage = MutableStateFlow("")
  val done = MutableStateFlow(false)

  fun setId(todoId: Int) {
    this.todoId.value = todoId
  }

  fun save(todo: ToDo, title: String, detail: String) {
    if (title.trim().isEmpty()) {
      errorMessage.value = "Input title"
      return
    }
    viewModelScope.launch {
      try {
        repo.update(todo, title, detail)
        done.value = true
      } catch (e: Exception) {
        errorMessage.value = e.message.toString()
      }
    }
  }
}

更新処理後の処理を実装する

こちらも作成の時と同様です。

@Composable
fun EditToDoScreen(
  navController: NavController,
  viewModel: EditToDoViewModel,
) {
  val scaffoldState = rememberScaffoldState()
  val todo = viewModel.todo.collectAsState(emptyToDo)

  if (todo.value._id == emptyToDoId) {
    Scaffold(
      scaffoldState = scaffoldState,
      topBar = {
        EditTopBar(navController, null)
      },
    ) {
      CircularProgressIndicator()
    }
    return
  }
  val title = rememberSaveable { mutableStateOf(todo.value.title) }
  val detail = rememberSaveable { mutableStateOf(todo.value.detail) }

  val errorMessage = viewModel.errorMessage.collectAsState()
  val done = viewModel.done.collectAsState()

  if (errorMessage.value.isNotEmpty()) {
    LaunchedEffect(scaffoldState.snackbarHostState) {
      scaffoldState.snackbarHostState.showSnackbar(
        message = errorMessage.value
      )
      viewModel.errorMessage.value = ""
    }
  }

  if (done.value) {
    // 再コンポーズ時にもう一度実行されたら困る
    viewModel.done.value = false
    navController.popBackStack()
  }

  Scaffold(
    scaffoldState = scaffoldState,
    topBar = {
      EditTopBar(navController) {
        viewModel.save(todo.value, title.value, detail.value)
      }
    },
  ) {
    EditToDoBody(title, detail)
  }
}

動作確認してみる

編集画面に遷移し、編集が完了すると詳細画面に戻ってきました。Roomの機能で最新のデータ取得が行われ、表示も更新されています。

ここまで作業したものはこちらにあります。

本サイトではサービス向上のため、Google Analyticsを導入しています。分析にはCookieを利用しています。