Memo: Kotlin + Room + RecyclerView + DataBinding (Part 2)

Notes on Data Binding with Kotlin and Recycler View w/ LiveData, DiffUtil

From the last time (Memo: Kotlin + Room + RecyclerView + DataBinding) Various support libraries have been added, so I will write it again.

environment

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

Thing you want to do

Source & Notes

DI part is omitted this time as well Since Dao (Room) and Repository are not created, only interface is used.

Repository Just a repository. I think it's not much different from 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 to get unread books from the repository

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

Inject the repository. Get unread Bookmarks as LiveData with ʻuncompletedItems (). You can get a list of Bookmarks from Room with Flowable by calling load ()`. Now when the Bookmark list is updated, you will be automatically notified where you have subscribed to incompletedItems ().

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

This is not connected to Room. Properties (text, ʻidtext) that mainly display list item data, It has a method that receives the interaction of list items. (ʻUpdate () ) For the time being, I updated the time when I clicked on the item.

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

This time's Kimo. Subscribe to the BookstackViewModel.uncompletedItems () created above at the beginning. At this time, AppCompatActivity is specified in the constructor in order to pass LifecycleOwner or Lifecycle.

Call ʻupdate () when the data is notified. In ʻupdate, DiffUtil is used to notify the change. LiveData is an update notification, so if you set the same value, you will be notified by the set amount. Since I want you to update the item only when the content of each item is changed, use DiffUtil to bindViewHolder only when it is changed.

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 abridgement

Fragment In Fragment, Adapter is set for RecyclerView. In addition, the list of unread books has started to be acquired for ViewModel.

BookstackFragment.kt


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

    //Abbreviation

    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()
        }
    }
    //Abbreviation
}

motion

croped.gif

Question

things to do

that's all.

Recommended Posts

Memo: Kotlin + Room + RecyclerView + DataBinding (Part 2)
[Memo] JSUG Study Group 2020 Part 1 Spring x Kotlin
Kotlin Memorandum-Code Edition (Part 1)