Mokelab Blog

#72 Jetpack ComposeでToDoアプリを作る - ToDoのリスト表示

データベースにToDoを追加するところまで作成できたので、次はリスト表示をやってみます。

ToDoの取得部分を作る

ToDoのリスト表示はメイン画面で行います。まずToDoの一覧を取得する部分を作ります。MainViewModelにFlowを返すプロパティを追加します。

@HiltViewModel
class MainViewModel @Inject constructor(
  private val repo: ToDoRepository
) : ViewModel() {
  val todoList = repo.getAll()
}

リポジトリにgetAll()がないので追加します。

class ToDoRepository {
  suspend fun create(title: String, detail: String): ToDo
  fun getAll(): Flow<List<ToDo>>
}

実装クラスではDAOのメソッドを呼ぶだけ。お手軽ですね。

class ToDoRepositoryImpl @Inject constructor(
  private val dao: ToDoDAO
) : ToDoRepository {
  ...
  override fun getAll(): Flow<List<ToDo>> {
    return dao.getAll()
  }
}

FlowをJetpack Composeの状態として扱う

ViewModelに追加したFlowはcollectAsState()を使うことでJetpack Composeの状態として扱うことができます。追加のライブラリは不要です。

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

StateなのでFlowが最初のデータを流してくるまでの間、初期値として使う値が必要になります。

リスト表示部分を作る

データベースの内容が状態として取得できるようになったので、次はリスト表示部分です。Scaffoldのボディ部分を作ります。

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

  Scaffold(
    topBar = { MainTopBar() },
    floatingActionButton = { MainFAB(navController) }
  ) {
    ToDoList(todoList)
  }
}

ToDoList() composableは次のようにしてみます。

@Composable
fun ToDoList(list: State<List<ToDo>>) {
  LazyColumn {
    items(list.value) { todo ->
      ToDoListItem(todo)
    }
  }
}

LazyColumn()は、上下方向のリスト表示をやってくれるComposableです。Listに入っているものをリスト表示したい場合はitems()を使います。

ToDoListItemはリスト1行分のレイアウトを作るComposableです。今回は次のように実装してみました。

@Composable
fun ToDoListItem(todo: ToDo) {
  Column(
    modifier = Modifier
      .fillMaxWidth()
      .heightIn(min = 48.dp)
      .padding(horizontal = 16.dp, vertical = 8.dp)
  ) {
    Text(
      todo.title,
      style = MaterialTheme.typography.subtitle1
    )
    Text(
      DateFormat.format("yyyy-MM-dd hh:mm:ss", todo.created).toString(),
      style = MaterialTheme.typography.body2
    )
  }
}

タイトル表示用のテキストと、作成時刻表示用のテキストを縦に並べたものにしてみました。

行タップ時の処理を実装する

最後に行タップ時の処理を実装します。Modifier.clickable()で、指定したComposableをタップ可能にすることができます。タップ時の処理は上位のComposableで記述できるよう、ラムダ式を受け取りそれを実行するだけにします。

@Composable
fun ToDoListItem(todo: ToDo, itemSelected: (todo: ToDo) -> Unit) {
  Column(
    modifier = Modifier
      .fillMaxWidth()
      .heightIn(min = 48.dp)
      .clickable { itemSelected(todo) }
      .padding(horizontal = 16.dp, vertical = 8.dp)
  ) {
    ...
  }
}

.padding()の後に.clickable()を記述すると、タップ可能な領域がパディングの内側のみになるので.padding()の前に.clickable()を記述するようにしましょう。

ToDoListItemにパラメータを追加したので、ToDoList()も修正します。

@Composable
fun ToDoList(list: State<List<ToDo>>, itemSelected: (todo: ToDo) -> Unit) {
  LazyColumn {
    items(list.value) { todo ->
      ToDoListItem(todo, itemSelected)
    }
  }
}

さらにMainScreen()も修正します。実際の処理は次回やります。

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

  Scaffold(
    topBar = { MainTopBar() },
    floatingActionButton = { MainFAB(navController) }
  ) {
    ToDoList(todoList) { todo ->
      // 画面遷移は次回
      println(it)
    }
  }
}

動作確認してみます。アダプターやViewHolderを作ることなく、リスト表示ができました。

動作確認

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

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