#57 AndroidでToDoアプリを作る - ToDoをリスト表示する
今回は、データベースに保存しているToDoをリスト表示してみます。
RecyclerView
RecyclerViewとは、大量のデータを効率的にリスト表示してくれるビューです。リストのスクロールによって表示されなくなったビューをリサイクルすることで、少ないメモリ消費量で大量のデータを扱えるようになっています。
RecyclerViewはいろんなライブラリの依存関係として追加されていますが、バージョンを固定するためにapp/build.gradleに追加します。
dependencies {
...
implementation "androidx.recyclerview:recyclerview:1.2.0-beta01"
}
次に、ToDoをリスト表示したい画面にRecyclerView
を貼ります。今回は起動直後の画面でリスト表示したいので、main_fragment.xml
に貼ります。画面全体に表示されるよう、制約をつけておきます。
- 上の辺を親の上の辺にあわせる
- 下の辺を親の下の辺にあわせる
- 左の辺を親の左の辺にあわせる
- 右の辺を親の右の辺にあわせる
- 幅と高さを0dpにする

IDは@+id/recycler
にしておきます。レイアウトXMLは次のようになります。
<androidx.constraintlayout.widget.ConstraintLayout
...
>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
...
/>
</androidx.constraintlayout.widget.ConstraintLayout>
1行分のレイアウトを作る
次に、1行分のレイアウトを作ります。res/layoutに、todo_item.xml
というレイアウトXMLを作ります。
まず、親となるConstraintLayoutの高さをmatch_parent
からwrap_content
にします。これを忘れるとリスト1行が1画面分として表示されてしまいます。また、高さの最小値を64dpにしておきます。
そして、TextViewを2つ並べ、次の表示となるよう制約やマージンなどをつけていきます。タイトル表示用のTextViewを@+id/title_text
、作成時刻表示用のTextViewを@+id/created_text
とします。

@+id/title_text
のほうには、次のように制約やマージンをつけていきます。
- 上の辺を親の上の辺にあわせる
- 左の辺を親の左の辺にあわせる
- 右の辺を親の右の辺にあわせる
- 上と、左右のマージンを16dpにする
-
textAppearanceを
?attr/textAppearanceSubtitle1
にする
@+id/created_text
のほうには、次のように制約やマージンをつけていきます。
- 上の辺をtitle_textの下の辺にあわせる
- 下の辺を親の下の辺にあわせる
- 左の辺を親の左の辺にあわせる
- 右の辺を親の右の辺にあわせる
- 下と、左右のマージンを16dpにする
- textAppearanceを
?attr/textAppearanceBody2
にする
レイアウトXMLは次のようになります。とても長いのでみたい方のみ表示ボタンを押してください。
アダプターを作る
1行分のレイアウトを作ったら、次はRecyclerView用のアダプターを作ります。RecyclerViewはアダプターに問い合わせをすることでリスト表示を実現しています。
手順が多いので順に説明します。まず、ListAdapter
を継承したクラスを作ります。同名のクラスが2つありますが、androidx.recyclerview.widget.ListAdapter
のほうを使います。
class ToDoAdapter: ListAdapter<ToDo, ToDoAdapter,ViewHolder>() {
}
この時点では、ToDoAdapterにViewHolderというクラスを作っていないので赤文字になります。ToDoAdapterの内部クラスとして作ります。
class ToDoAdapter : ListAdapter<ToDo, ToDoAdapter.ViewHolder>() {
class ViewHolder(
private val binding: TodoItemBinding
) : RecyclerView.ViewHolder(binding.root) {
}
}
ViewHolderとは、1行分のビューを保持しておくためのクラスです。値をセットする際、毎回findViewById()
を呼ぶと時間がかかってしまうため、あらかじめ値をセットするためのビューを取り出しておきます。
まだまだ赤線は続きます。ListAdapterのプライマリーコンストラクタは引数を1つ必要とします。要素を比較するためのオブジェクトを渡す必要があるので、companion object
の中で作ります。そしてプライマリーコンストラクタの引数に渡します。
class ToDoAdapter : ListAdapter<ToDo, ToDoAdapter.ViewHolder>(callbacks) {
...
companion object {
private val callbacks = object : DiffUtil.ItemCallback<ToDo>() {
// 同じアイテムかどうかを調べる
override fun areItemsTheSame(oldItem: ToDo, newItem: ToDo): Boolean {
return oldItem._id == newItem._id
}
// 同じアイテムの時に、表示内容が同じかどうかを調べる
override fun areContentsTheSame(oldItem: ToDo, newItem: ToDo): Boolean {
return oldItem.title == newItem.title &&
oldItem.created == newItem.created
}
}
}
}
areItemsTheSame
は2つのアイテムが同じものかを調べます。今回はToDoのIDが同じかどうかで調べます。areContentsTheSame
は2つのアイテムの表示内容が同じかどうかを調べます。こちらはareItemsTheSame
がtrue
を返したときに呼ばれ、リストの表示内容を更新すべきかどうかの判定で使われます。
最後に、実装すべきメソッドを実装します。class ToDoAdapterのToDoAdapterのところにカーソルを移動させ、Alt+Enterで必要となるメソッドを追加します。

onCreateViewHolder
では、1行分のビューを作り、先ほど定義したViewHolderに詰めて返します。
class ToDoAdapter : ListAdapter<ToDo, ToDoAdapter.ViewHolder>(callbacks) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
// parent, false で呼ぶ
val binding = TodoItemBinding.inflate(inflater, parent, false)
return ViewHolder(binding)
}
...
}
onBindViewHolder
では、ViewHolderとリスト中での場所が渡されます。ここではビューに値をセットしていきます。getItem(position)
で、アダプターにセットされたToDoを取得することができます。ViewHolderに設定メソッドを作り、その中で行うとコードがすっきりするでしょう。
class ToDoAdapter : ListAdapter<ToDo, ToDoAdapter.ViewHolder>(callbacks) {
...
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val todo = getItem(position)
holder.bindTo(todo)
}
class ViewHolder(
private val binding: TodoItemBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bindTo(todo: ToDo) {
binding.titleText.text = todo.title
binding.createdText.text =
DateFormat.format("yyyy-MM-dd hh:mm:ss", todo.created)
}
}
...
}
これでアダプターができました。
RecyclerViewの準備をする
ToDoAdapterができたら、フラグメント側でRecyclerViewにセットします。
@AndroidEntryPoint
class MainFragment : Fragment(R.layout.main_fragment) {
...
private var _binding: MainFragmentBinding? = null
private val binding: MainFragmentBinding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
this._binding = MainFragmentBinding.bind(view)
...
val adapter = ToDoAdapter()
binding.recycler.adapter = adapter
}
...
}
そしてビューの並べ方を指示するためのレイアウトマネージャを指定します。Kotlinコードでも指定できますが、縦一列に並べるだけの場合はレイアウトXMLで指定することもできます。
main_fragment.xmlで、RecyclerViewにapp:layoutManager
属性を追加します。
<androidx.constraintlayout.widget.ConstraintLayout
...
>
<androidx.recyclerview.widget.RecyclerView
...
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
...
/>
</androidx.constraintlayout.widget.ConstraintLayout>
ToDoを取得しアダプターにセットする
最後にToDoをリポジトリから取得し、アダプターにセットする部分です。
まず、ToDoRepository
にToDoをすべて取得するためのメソッドを追加します。
interface ToDoRepository {
suspend fun create(title: String, detail: String)
fun getAll(): Flow<List<ToDo>>
}
そして、ToDoRepositoryImpl
で中身を実装します。今回はDAOからとってきたものをそのまま返すことにします。
class ToDoRepositoryImpl @Inject constructor(
private val dao: ToDoDAO
) : ToDoRepository {
...
override fun getAll(): Flow<List<ToDo>> {
return dao.getAll()
}
}
ToDoDAO
にgetAll()
がなかったので、こちらも作ります。
@Dao
interface ToDoDAO {
...
@Query("select * from ToDo order by created desc")
fun getAll(): Flow<List<ToDo>>
...
}
リポジトリ側の準備ができたので、次はFlow<T>
をLiveDataにしてViewModelにもたせます。lifecycle-livedata-ktxにasLiveData()
という便利な拡張関数が用意されているので、app/build.gradleのdependenciesに追加します。
dependencies {
...
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0'
}
追加したら右上の「Sync now」を押すのを忘れないようにしましょう。
MainViewModel
のコンストラクタでリポジトリを受け取り、次のようにLiveDataをプロパティとしてもたせます。
@HiltViewModel
class MainViewModel @Inject constructor(
private val repo: ToDoRepository
) : ViewModel() {
val todoList = repo.getAll().asLiveData()
}
最後にフラグメント側でこのLiveDataを監視します。リポジトリから新しいToDoのリストを受け取ったら、submitList()
でアダプターに渡します。
@AndroidEntryPoint
class MainFragment : Fragment(R.layout.main_fragment) {
private val vm: MainViewModel by viewModels()
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
val adapter = ToDoAdapter()
binding.recycler.adapter = adapter
vm.todoList.observe(viewLifecycleOwner) { list ->
adapter.submitList(list)
}
}
...
}
動作確認してみる
ここまでできたらアプリを実行してみましょう。追加したToDoがリストで表示されれば成功です。

さらにToDoを1つ追加し、表示させてみます。

ちゃんと2件表示されました。
まとめ
RecyclerViewを使ってToDoをリスト表示させてみました。ListAdapterは準備が大変ですが、リストの差分を効率的に扱ってくれるので、使えるようになっておきましょう。