Java
Android
DataBinding

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

KotlinとRecyclerViewとDataBindingしたときのメモ
w/ LiveData, DiffUtil

前回(メモ:Kotlin+Room+RecyclerView+DataBinding)から、
いろいろとサポートライブラリが追加されていたので再度書いてみる。

環境

Library Version -
AndroidStudio 3.2 Beta 2
Kotlin 1.2.51
Room 1.0.0 not use
RxJava2 2.1.16
Dagger2 2.15
SupportLibrary 28.0.0-alpha3

やりたいこと

  • Kotlin+Roomでテーブルへの更新があると自動的にRecyclerViewへ反映させたい。
  • RecyclerViewの各要素をViewModelとDataBindingしたい(もっと楽に)

ソース & メモ

今回もDI部分は省略
Dao(Room) , Repositoryは作ってないので、interfaceのみとした。

Repository

ただのリポジトリ。
Daoとあんまり変わらない気がするけど。

BookmarkRepository.kt
interface BookmarkRepository{
    fun bookmark(bookmarkId: String): Single<Bookmark>
    fun bookmarks(userId: String): Flowable<List<Bookmark>>
    fun store(bookmark: Bookmark): Completable
}

Usecase

リポジトリから未読の本を取得するUsecase

class GetReadingAndUnreadBooks(private val repository: BookmarkRepository){

    fun execute(params: Params): Flowable<List<Bookmark>> =
        repository.bookmarks(params.userId)
                .map { it.filter { isReading(it) }
                        .sortedBy { it.state().sort }
                        .sortedByDescending { it.latestDate() }
                }
                .distinctUntilChanged()

    private fun isReading(bookmark: Bookmark): Boolean =
            when (bookmark.state()) {
                ReadState.Unread -> true
                ReadState.Reading -> true
                else -> false
            }

    class Params(val userId: String)
}

ViewModel

  • List用ViewModel

リポジトリはInjectする。
uncompletedItems()で、未読のBookmarkをLiveDataとして取得する。
load()を呼ぶことで、RoomからFlowableでBookmarkのリストを取得できる。
これでBookmarkのリストが更新されたときに、uncompletedItems()を購読している箇所に自動的に通知される。

BookstackViewModel.kt
class BookstackViewModel @Inject constructor(private val bookmarkRepository: BookmarkRepository) : ViewModel() {

    private val usecase =  GetReadingAndUnreadBooks(bookmarkRepository)

    val userId = MutableLiveData<String>()

    private val _uncompletedItems = MutableLiveData<List<Bookmark>>()

    fun uncompletedItems() = _uncompletedItems as LiveData<List<Bookmark>>

    fun load(): Disposable {
        val id = userId.value?: throw IllegalStateException("User id is must be not null.")
        return usecase.execute(GetReadingAndUnreadBooks.Params(id))
                .subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe {
                    _uncompletedItems.postValue(it)
                }
    }
}
  • ListItem用ViewModel

こちらはRoomとの接続はしていない。
主にリストアイテムのデータを表示するプロパティ(text,idtext)と、
リストアイテムのインタラクションを受けるメソッドを生やしている。(update())
とりあえずアイテムをクリックしたら時間を更新するようにした。

UnreadItemViewModel.kt
class UnreadItemViewModel: ViewModel() {

    val text = MutableLiveData<String>()
    val idText = MutableLiveData<String>()

    private val item = MutableLiveData<Bookmark>().apply {
        this.observeForever {
            it?.apply {
                text.postValue(this.comment)
                idText.postValue(this.id)
            }
        }
    }
    fun setBookmark(bookmark: Bookmark) {
        item.postValue(bookmark)
    }
    fun bookmark() = item as LiveData<Bookmark>
    fun update() {
        item.value?.copy(comment =  SimpleDateFormat("yyyy/mm/dd HH:mm:ss").format(Date()))?.apply {
            item.postValue(this)
        }

    }
}

RecyclerViewAdapter

今回のキモ。
上で作った BookstackViewModel.uncompletedItems() を初っ端で購読する。
この時、LifecycleOwnerまたはLifecycleを渡すために、AppCompatActivityをコンストラクタに指定している。

データが通知されたらupdate()を呼び出す。
update内では、DiffUtilを使用して、変更通知を行っている。
LiveDataは更新通知であるため、同一の値を設定すると設定した分だけ通知されてしまう。
各アイテムの内容が変更された場合のみアイテムを更新してほしいため、DiffUtilを使って変更時のみbindViewHolderする。

BookstackRecyclerViewAdapter.kt
class BookstackRecyclerViewAdapter(val context: AppCompatActivity, viewModel: BookstackViewModel): RecyclerView.Adapter<BookstackRecyclerViewAdapter.ViewHolder>() {

    init {
        viewModel.uncompletedItems().observe({ context.lifecycle }, {it?.apply {
            update(this)
        }})
    }

    private lateinit var recyclerView: RecyclerView

    private var items: MutableList<Bookmark> = arrayListOf()

    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        super.onAttachedToRecyclerView(recyclerView)
        this.recyclerView = recyclerView
    }

    private fun update(list: List<Bookmark>) {
        val adapter = recyclerView.adapter as BookstackRecyclerViewAdapter
        val diff = DiffUtil.calculateDiff(DiffCallback(adapter.items, list))
        diff.dispatchUpdatesTo(adapter)
        this.items.clear()
        this.items.addAll(list)
    }

    override fun getItemCount() = this.items.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val vm = UnreadItemViewModel()
        vm.setBookmark(items[position])
        holder.binding.vm = vm
    }

    override fun onCreateViewHolder(parent: ViewGroup, position: Int): ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val binding: ItemUnreadBookBinding = DataBindingUtil.inflate(inflater, R.layout.item_unread_book, parent, false)
        binding.setLifecycleOwner(context)
        return ViewHolder(binding)
    }

    class ViewHolder(val binding: ItemUnreadBookBinding): RecyclerView.ViewHolder(binding.root)

    class DiffCallback(private val oldList: List<Bookmark>, private val newList: List<Bookmark>): DiffUtil.Callback() {

        override fun areContentsTheSame(oldPosition: Int, newPosition: Int) = oldList[oldPosition] == (newList[newPosition])

        override fun areItemsTheSame(oldPosition: Int, newPosition: Int) = oldList[oldPosition].id == (newList[newPosition]).id

        override fun getNewListSize() = newList.size

        override fun getOldListSize() = oldList.size
    }
}

Activity

省略

Fragment

Fragmentでは、RecyclerViewに対してAdapterを設定している。
また、ViewModelに対して、未読本のリストを取得開始している。

BookstackFragment.kt
class BookstackFragment : Fragment(), Injectable {
   private lateinit var binding: BookstackFragmentBinding

    // 略

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

        this.binding = DataBindingUtil.inflate(inflater, R.layout.bookstack_fragment, container, false)
        this.binding.setLifecycleOwner(this.activity as AppCompatActivity)
        this.binding.list.adapter = BookstackRecyclerViewAdapter(this.activity as AppCompatActivity, viewModel)
        this.binding.list.layoutManager = LinearLayoutManager(context)
        this.binding.list.setHasFixedSize(true)

        return binding.root
    }

    override fun onStart() {
        super.onStart()
        disposable = viewModel.load()
    }

    override fun onStop() {
        super.onStop()
        disposable?.apply {
            if (isDisposed) return
            dispose()
        }
    }
    // 略
}

動作

croped.gif

疑問点

  • 各リストアイテムのViewModelってnewしていいのだろうか?
    ViewModelProviders.of(context).get(ViewModel::class)を使うと、
    インスタンスが使いまわされるので全アイテムが同じものになってしまう。
  • BookstackRecyclerViewAdapterのinitでuncompletedItemsを購読しているけど、ここでいいのだろうか。

やること

  • DiffCallback.getChangePayload()の使い方がわからないので調べる
  • Roomとつなげてみて、各アイテムをクリックしたときにDBを変更するようにして自動反映されるか確認する

以上。