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とあんまり変わらない気がするけど。
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()を購読している箇所に自動的に通知される。
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()
)
とりあえずアイテムをクリックしたら時間を更新するようにした。
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する。
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に対して、未読本のリストを取得開始している。
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()
}
}
// 略
}
動作
疑問点
- 各リストアイテムのViewModelってnewしていいのだろうか?
ViewModelProviders.of(context).get(ViewModel::class)を使うと、
インスタンスが使いまわされるので全アイテムが同じものになってしまう。 - BookstackRecyclerViewAdapterのinitでuncompletedItemsを購読しているけど、ここでいいのだろうか。
やること
- DiffCallback.getChangePayload()の使い方がわからないので調べる
- Roomとつなげてみて、各アイテムをクリックしたときにDBを変更するようにして自動反映されるか確認する
以上。