#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
}
}
ToDoRepository
にgetById()
がないので作ります。
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
)
動作確認をしてみます。ちゃんと画面遷移して詳細表示されました。
ここまで作業したものはこちらにあります。