Mokelab Blog

#73 Jetpack ComposeでToDoアプリを作る - ToDoの詳細表示

今回はToDoの詳細表示を実装してみます

パラメータ付きで画面遷移する

Navigation ComposeではパラメータはURLのパスのような文字列で渡す必要があります。タップされたToDoのIDを詳細画面にわたすようにしてみます。

@Composable
fun MainScreen(
  navController: NavController,
  viewModel: MainViewModel,
) {
  val todoList = viewModel.todoList.collectAsState(emptyList())

  Scaffold(
    topBar = { MainTopBar() },
    floatingActionButton = { MainFAB(navController) }
  ) {
    ToDoList(todoList) { todo ->
      // IDをパスパラメータとしてセット
      navController.navigate("detail/${todo._id}")
    }
  }
}

パラメータを受け取る

パラメータの受け取りはNavHostの箇所で行います。当初はScreen composableにIDを渡す方式を考えてみましたが、ViewModelに渡す方式がよさそうだったので変更しています。

@Composable
fun ToDoApp() {
  val navController = rememberNavController()

  ComposeToDoTheme {
    NavHost(navController = navController, startDestination = "main") {
      ...
      composable(
        "detail/{todoId}",
        arguments = listOf(navArgument("todoId") { type = NavType.IntType })
      ) { backStackEntry ->
        val viewModel = hiltViewModel<ToDoDetailViewModel>()
        val todoId = backStackEntry.arguments?.getInt("todoId") ?: 0
        // ViewModelに渡す
        viewModel.setId(todoId)
        ToDoDetailScreen(
          navController = navController,
          viewModel = viewModel,
        )
      }
    }
  }
}

setId()では次のようにMutableStateFlowにいれます。MutableStateFlowは値が変化したときのみデータを流すという性質があるので、再コンポーズで同じ値が何度もセットされても、後続の処理には1度しかIDが流れません。

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

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

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

ToDoRepositorygetById()がないので作ります。

interface ToDoRepository {
  ...
  fun getById(todoId: Int): Flow<ToDo>
}
class ToDoRepositoryImpl @Inject constructor(
  private val dao: ToDoDAO
) : ToDoRepository {
  ...
  override fun getById(todoId: Int): Flow<ToDo> {
    // 0件だったときを考える必要はある。。。
    return dao.getById(todoId).take(1).map { list -> list[0] }
  }
}

画面レイアウトを作る

ToDoDetailScreen用の画面レイアウトを作ります。

@Composable
fun DetailBody(todo: ToDo) {
  Column {
    Text(
      todo.title,
      style = MaterialTheme.typography.h3,
      modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)
    )
    Text(
      todo.detail,
      style = MaterialTheme.typography.body1,
      modifier = Modifier
        .weight(1.0f, true)
        .padding(horizontal = 8.dp, vertical = 8.dp)
    )
  }
}

ルートとなるComposableでは、ID指定でとってきたToDoを状態として受け取るようにします。例によって初期値が必要なので、読み込み中を表すIDをもつToDoを仮としてセットしておきます。

@Composable
fun ToDoDetailScreen(
  navController: NavController,
  viewModel: ToDoDetailViewModel,
) {
  val todo = viewModel.todo.collectAsState(emptyToDo)

  Scaffold(
    topBar = {
      // ここは次回紹介します
      DetailTopBar(navController, todo.value)
    },
  ) {
    DetailBody(todo.value)
  }
}

private const val emptyToDoId = -1
private val emptyToDo = ToDo(
  _id = emptyToDoId,
  title = "",
  detail = "",
  created = 0,
  modified = 0
)

動作確認をしてみます。ちゃんと画面遷移して詳細表示されました。

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

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