Android
Kotlin
メモ
Room

メモ:Kotlin+Room+RecyclerView+DataBinding

KotlinとRoomでDataBindingした時のRecyclerView実装を試してみたのでメモ。

続き→ メモ:Kotlin+Room+RecyclerView+DataBinding(その2)

環境

Library Version
AndroidStudio 3.0.1
Kotlin 1.2.30
Room 1.0.0
RxJava2 2.1.10
Dagger2 2.15
SupportLibrary 26.1.0

やりたいこと

Kotlin+Roomでテーブルへの更新があると自動的にRecyclerViewへ反映させたい。

メモ

今回はRecyclerViewのところだけにする。Dagger2はまたの機会に。

Dao

RoomのDaoクラス。(一部)
interfaceにFlowableを指定すると、SQLite3上で変更を検知したらonNextで流してくれる。

Room上で使用できるRxJavaのクラスは以下の通り。

Class データがない時 データがある時 変更通知
Maybe onComplete onSuccess No
Single onError onSuccess No
Flowable no action onNext Yes


RoleDao.kt
@Dao
abstract class RoleDao {

    @Query("SELECT * FROM role WHERE id = :id")
    abstract fun find(id: Long): Single<RoleEntity>

    @Query("SELECT * FROM role")
    abstract fun findAll(): Flowable<List<RoleEntity>>

// […]

Model

普通のDataClassを作る。

Role.kt
data class Role(val id: Long?
                , val name: String
                , val description: String
                , val isLock: Boolean): BaseObservable()

Repository

※抜粋
リポジトリはDaoの戻り値Flowable<List<RoleEntity>>Flowable<List<Role>>へ変換するだけ。

RoleRepository.kt
fun RoleEntity.toRole() = Role(id , name, description, isLock)

class RoleRepository @Inject constructor(private val database: AppDatabase): Repository<Role> {
 // […]
    override fun all() = database.roleDao() 
            .findAll()                 // Flowable<List<RoleEntity>>
            .map {
                it.map { it.toRole() } // return Flowable<List<Role>>
            }
 // […]
}

ViewModel

RecyclerViewのAdapterに必要な件数とアイテム取得メソッドを作成。
DataBinding用のリスナ追加も行う。

RoleViewModel.kt
class RoleViewModel @Inject constructor (private val repo: RoleRepository): ViewModel(), LifecycleObserver {

    private val mRoleList: ObservableList<Role> = ObservableArrayList()

    fun initializeList() = repo.removeAll().subscribeOn(Schedulers.io())

    fun getItem(index: Int) = mRoleList[index]

    fun count() = mRoleList.size

    fun addRoleItemChangeListener(listener: ObservableList.OnListChangedCallback<ObservableList<Role>>) {
        mRoleList.addOnListChangedCallback(listener)
    }

    fun addRole(roleName: String) = repo
            .store(Role(id = null, name = roleName, description = roleName, isLock = false))
            .flatMap {
                repo.resolve(it)
            }
            .subscribeOn(Schedulers.io())
}

Adapter

RecyclerViewとViewModelを引っ付ける。
ViewModel.addRoleItemChangeListener()したことにより、通知によってViewが更新されるようになる。

RoleBindingRecyclerViewAdapter.kt
class RoleBindingRecyclerViewAdapter(val viewModel: RoleViewModel) : RecyclerView.Adapter<RoleBindingRecyclerViewAdapter.BindingHolder>() {
    init {
        viewModel.addRoleItemChangeListener(object: ObservableList.OnListChangedCallback<ObservableList<Role>>() {
            override fun onChanged(list: ObservableList<Role>?) {
                notifyDataSetChanged()
            }
            override fun onItemRangeChanged(p0: ObservableList<Role>?, positionStart: Int, itemCount: Int) {
                notifyItemRangeChanged(positionStart, itemCount)
            }
            override fun onItemRangeInserted(p0: ObservableList<Role>?, positionStart: Int, itemCount: Int) {
                notifyItemRangeInserted(positionStart, itemCount)
            }
            override fun onItemRangeMoved(p0: ObservableList<Role>?, fromPosition: Int, toPosition: Int, itemCount: Int) {
                for (i in 0..itemCount) {
                    notifyItemRangeRemoved(fromPosition + i, toPosition + i)
                }
            }
            override fun onItemRangeRemoved(p0: ObservableList<Role>?, positionStart: Int, itemCount: Int) {
                notifyItemRangeRemoved(positionStart, itemCount)
            }
        })
    }

    override fun getItemCount() = viewModel.count()

    override fun onBindViewHolder(holder: BindingHolder, position: Int) {
        holder.binding.apply {
            setVariable(BR.role, viewModel.getItem(position))
            notifyChange()
            executePendingBindings()
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): BindingHolder {
        val inflater = LayoutInflater.from(parent!!.context)
        val viewDataBinding: ViewDataBinding = DataBindingUtil.inflate(inflater, R.layout.item_role, parent, false)
        return BindingHolder(viewDataBinding)
    }

    class BindingHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
}

Fragment

ViewModelをどっかからとってきて、Adapterにセットする。

RoleSettingFragment.kt
class RoleSettingFragment : DaggerFragment(), Injectable {

    private lateinit var binding: FragmentRoleSettingBinding

    @Inject
    lateinit var viewModelFactory: ViewModelFactory

    private val roleViewModel: RoleViewModel by lazy {
        ViewModelProviders.of(this, viewModelFactory).get(RoleViewModel::class.java)
    }

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_role_setting, container, false)

        binding.recyclerView.layoutManager = LinearLayoutManager(this.context)
        binding.recyclerView.adapter = RoleBindingRecyclerViewAdapter(roleViewModel)

        // For Sample
        binding.imageAdd.setOnClickListener {
            binding.viewModel?.apply {
                this.addRole("ITEM:${System.currentTimeMillis()}")
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe{item ->
                            Snackbar.make(binding.root, "Created. ${item.name}", Snackbar.LENGTH_SHORT).show()
                        }
            }
        }
        return binding.root
    }
}

Layout XML

[省略]

思ったこと

割と簡単にできた、DataBindingすごい。
Adapterとか昔はもっとゴリゴリ書いてたような気がする。

RoomがFlowableで返せることをつかってるので、Realmとかでもできるはず。
多分だれかが書いてる。。。
いまのORMはどれもできるのかなあ・・・?

今回、RecyclerViewというよりDagger2が厄介だった。
次はDagger2に関するメモを書こう。

参考

RecyclerViewのDataBinding
Room 🔗 RxJava
DroidKaigi 2018 official Android app