メモ：Kotlin+Room+RecyclerView＋DataBinding

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

続き→ [メモ：Kotlin+Room+RecyclerView＋DataBinding（その２）](https://qiita.com/nitakan/items/b57938702fe5bd964e3f)

## 環境

|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|

<br />

``` 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](https://qiita.com/nagasakulllo/items/88c7857c4b542825b06b) 
[Room 🔗 RxJava](https://medium.com/google-developers/room-rxjava-acb0cd4f3757)
[DroidKaigi 2018 official Android app](https://github.com/DroidKaigi/conference-app-2018)