30歳からの開発日記

30歳でエンジニア転職した元営業の備忘録。

【Android】RecyclerView のリスト表示を MVVM パターン+ DataBinding で実装する

今回やること

タイトルどおり、RecyclerView をMVVM パターン+ DataBinding で実装してみたいと思います。

前回の記事では双方向バインディングのサンプルアプリを作ってみました。これは Text 型の LiveData を TextView にバインドして、値が変わったら自動的に画面に反映するような実装でした。これと同じようなことを RecyclerView でやろうとすると、BindingAdapter の実装などちょっとした工夫が必要です。

BindingAdapter を使えがば RecyclerView に対して「この値が変わったら画面を更新してね!」と指示を出すことができます。つまり、これまで findViewById() で取得したビューをゴニョゴニョしていた処理を書かなくて済むので、少し幸せになれます。

今回はやることをざっくり言うとこんな感じです。

  • ViewModel に RecyclerView で表示したいリストアイテムを持たせる
  • レイアウトファイル内で RecyclerView にリストアイテムをバインドする
  • クリックでリストアイテムの中身を変更するボタンを設置(API に問い合わせる想定)
  • リストアイテムの変更で自動的に画面も変更!!

見た目は普通の RecyclerView です。

ソースファイルはこちらで公開してます。

github.com

最終的なディレクトリ構造

参考までに最終的なディレクトリ構造を載せておきます。

実装手順

1. 新しいプロジェクトを作成

まずは Empty Activity で新しいプロジェクトを作成します。

2. Gradel ファイルを編集

build.gradle (Module: app) を編集してデータバインディングの有効化とライブラリの追加を行います。

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' // 追加

android {
    ...

    // dataBinding を有効化
    dataBinding {
        enabled = true
    }

}

dependencies {
    ...
    
    // ViewModel と LiveData のライブラリを追加
    def lifecycle_version = "2.2.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
}

3. ViewModel、User モデルクラスを作成

次に ViewModel と User モデルクラスを作成します。

UserViewModel という名前で Kitlin ファイルを作成して以下のとおり編集します。

/**
 * User モデルクラス
 */
data class User(var name: String)

/**
 * User の情報を保持する ViewModel。
 */
class UserViewModel : ViewModel() {

    // クラス内部で扱う LiveData は変更可能な形式で保持する。
    private val _users: MutableLiveData<List<User>> = MutableLiveData()
    // クラス外部に公開する LiveData は変更不可な形式で保持する。
    val users: LiveData<List<User>> = _users

    // 初期値として10人分のユーザー情報を LiveData に突っ込む。
    init {
        val names = listOf(
            "一郎", "二郎", "三郎", "四郎", "五郎", "六郎", "七郎", "八郎", "九郎", "十郎"
        )
        val users = names.map { name -> User(name) }
        _users.value = users
    }

    /**
     * API 通信してユーザー情報を取ってくることを模した関数。
     * 今回は通信せずにリストをシャッフルする仕様にしている。
     */
    fun fetchUsers() {
        val shuffled = _users.value?.shuffled()
        _users.value = shuffled
    }
}

4. レイアウトファイルを作成、編集

activity_main.xml

リストの中身を更新するためのボタンと RecyclerView を設置します。

<?xml version="1.0" encoding="utf-8"?>
<!-- データバインディングを行うため、ルートを layout タグにする。 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- レイアウトファイル内で使う変数を data タグの中で定義する。 -->
    <data>
        <!-- 変数 viewModel を定義する。 -->
        <variable
            name="viewModel"
            type="com.example.bindingadaptersample.UserViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <!-- ボタンクリックで UserViewModel#fetch を発火させる。 -->
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{() -> viewModel.fetchUsers()}"
            android:text="FETCH!" />

        <!-- BindingAdapter を利用して users という属性を自作している。 -->
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:users="@{viewModel.users}"
            tools:context=".MainActivity" />

    </LinearLayout>

</layout>

row_user_list.xml

リスト1行分のレイアウトファイルを新規作成します。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:id="@+id/user_name"
        android:layout_width="wrap_content"
        android:layout_height="80dp"
        android:gravity="center_vertical"
        android:padding="8dp"
        android:textSize="30sp"
        android:textStyle="bold" />

</layout>

5. リストアダプターを作成

UserListAdapter という名前で Kotlin ファイルを追加して以下のとおり編集します。

/**
 * ユーザーリスト用の Adapter。
 */
class UserListAdapter(
    private var users: List<User>,
    private val owner: LifecycleOwner
) : RecyclerView.Adapter<UserListAdapter.UserViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val binding: RowUserListBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.row_user_list,
            parent,
            false
        )
        return UserViewHolder(binding)
    }

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        val user = users[position]
        holder.binding.apply {
            userName.text = user.name
            lifecycleOwner = owner
        }
    }

    override fun getItemCount(): Int {
        return users.size
    }

    /**
     * リストの中身を更新する。
     */
    fun update(users: List<User>) {
        this.users = users
        notifyDataSetChanged()
    }

    /**
     * ユーザーリスト用の ViewHolder。
     */
    inner class UserViewHolder(val binding: RowUserListBinding) :
        RecyclerView.ViewHolder(binding.root)
}

6. BindingAdapter を作成

UserListAdapters という名前で Kotlin ファイルを追加して以下のとおり編集します。

RecyclerView を拡張した関数なので adapter が取得できます。ただし、もし複数の RecyclerView に対して app:users="@{xxx}" を設定していた場合、それがどの RecyclerView なのか判別する必要があります。そのために when() で型をチェックしてから adapter.update() を実行しています。

object BindingAdapters {
    /**
     * RecyclerView の拡張関数を定義。
     * レイアウトファイル内で users という属性を設定できるようにする。
     */
    @BindingAdapter("users")
    @JvmStatic fun RecyclerView.bindUsers(users: List<User>?) {
        users ?: return
        // adapter が UserListAdapter ならリスト情報をアップデートする。
        when (val adapter = this.adapter) {
            is UserListAdapter -> adapter.update(users)
        }
    }
}

7. MainActivity を編集

最後の仕上げに MainActivity を編集します。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // UserViewModel を取得。
        val viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
        // gradle ファイルで DataBinding を有効にするとバインディングクラスが自動生成される。
        val binding: ActivityMainBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_main)

        binding.also {
            it.viewModel = viewModel // レイアウトファイルで定義した viewModel にバインド。
            it.lifecycleOwner = this
        }

        val users = viewModel.users.value ?: listOf()
        val adapter = UserListAdapter(users, this)
        val manager = LinearLayoutManager(this)
        val decoration = DividerItemDecoration(this, manager.orientation)
        binding.recyclerView.also {
            it.adapter = adapter
            it.layoutManager = manager
            it.addItemDecoration(decoration) // リストアイテム間に境界線をつける。
        }
    }
}

処理の流れ

画面が更新される流れをざっくり表すと以下のようになります。

  1. ボタンをクリック
  2. UserViewModel#fetch が発火
  3. UserViewModel のメンバ変数 users の値が変化
  4. 値の変化を検知して RecyclerView#bindUsers() が発火
  5. UserListAdapter#update() が発火して画面を更新

今回は参考になる記事が少なく少し苦労しましたが、とても便利そうなのでどんどん使っていきたいと思います。