mikepenz / FastAdapter

The bullet proof, fast and easy to use adapter library, which minimizes developing time to a fraction...
https://mikepenz.dev
Apache License 2.0
3.83k stars 493 forks source link

Architecture Components ViewModel for fastadapter status #612

Closed gpulido closed 6 years ago

gpulido commented 6 years ago

Hello @mikepenz I'm refactoring one of my apps to use MVVM + Databinding + Realm and I would like to use the fastadapter for the recyclerview management. I'm using a modelabstract to wrap my realm models and I didn't have any trouble binding the viewholders to the xml and the realm data to be used by the fasteradapter So for the moment so far so good. Now I'm adding multiselection support, and I facing a architectural doubt: If I follow the MVVM principles I must maintain the selection status on the Viewmodel so the actions that affect the "selection" could be tested without ui, this means that I have to come with a way to maintain the fasteradapter "status" in the viewmodel and to restore it when the ui is reconstructed. Probably what I'm saying is that I have to substitute the bundle args with a ViewModel structure for the adapter status. In order to do it, I would need to "duplicate" the way the fastadapter stores and retrieve such info from the bundle args, and also keep it updated. Probably it would be enough if I move the "multi selection helper" onto the viewmodel for the Fragment where my recyclerview lives.

Also, I wonder if the fastadapter items could behave as VM by themselves and to be linked to the activity / fragment livecycle. In a purist MVVM each recyclerview item should have its own VM to back the status (isselected for example). Just some thoughts regarding MVVM

gpulido commented 6 years ago

Hello again, If it is ok, I'm going to autoanswer me and write down the code that ended working for me. Hopefully this will help others, or maybe it could point some things that could be done better from my part. Don't hesitate on make any comments.

As a first step, I want to maintain the selected fasteradapter object list "state" on the Fragmentviewmodel. This way it will survive as long as the viewmodel where I also maintain the list of objects to be used by the adapter. So the lifecycle of both list will be the same (from my POW, this is what have more sense)

This is the viewModel part:

abstract class RealmObjectListViewModel<TRealmObject> : RealmViewModel()
        where TRealmObject : RealmModel,
              TRealmObject : IHasPrimarykey
{
    abstract var title : String

    var results: LiveData<RealmResults<TRealmObject>>
    //TODO: use livedata for the selectedojectsList
    var selectedObjects:List<TRealmObject> = ArrayList()
    abstract fun viewModelDao(): RealmObjectDao<TRealmObject>
    init {
        results = viewModelDao().loadObjectsAsync()
    }
}

The viewmodel is inflated on the onActivityCreated method of the fragment:

 override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        archerListViewModel = ViewModelProviders.of(this).get(ArcherListViewModel::class.java)
        binding.let{
            it.viewModel = archerListViewModel
            it.setLifecycleOwner(this)
        }

        binding.recyclerView.let {
            it.adapter = fastAdapter
            it.layoutManager = LinearLayoutManager(context)
        }      
    }

The configuration of the fastAdapter is done through the following code:

 private fun generateFastAdapter(): FastAdapter<ArcherItem> {
        val itemAdapter = ModelAdapter(IInterceptor<Archer, ArcherItem> { o -> ArcherItem(o as Archer) })
        val fastAdapterTemp: FastAdapter<ArcherItem> = FastAdapter.with(itemAdapter)
        actionModeHelper = ActionModeHelper(fastAdapter, R.menu.contenders_actionmode, ActionBarCallBack())
        fastAdapterTemp.withSelectable(true)
        fastAdapterTemp.withMultiSelect(true)
        fastAdapterTemp.withSelectOnLongClick(true)
        fastAdapterTemp.withSelectionListener { item, selected ->
            archerListViewModel.selectedObjects = fastAdapter.selectedItems.map({it.archer})
            fastAdapter.notifyAdapterItemChanged(fastAdapter.getPosition(item!!))
        }     

        fastAdapterTemp.withOnPreClickListener { v, adapter, item, position ->
            val res: Boolean? = actionModeHelper.onClick(item)
            if (res != null) res else false
        }

        fastAdapterTemp.withOnPreLongClickListener { v, adapter, item, position ->
            val actionMode = actionModeHelper.onLongClick(activity as AppCompatActivity, position)
            actionModeHelper.isActive
        }
        return fastAdapterTemp

    }

The important line is the subscription to the selection listener that will update the VM. In this case I become a bit lazy and just substitute the entire list. In the future I will just change the items

 archerListViewModel.selectedObjects = fastAdapter.selectedItems.map({it.archer})

This ensures the adapter -> VM comunication.

Now we need to make sure that the adapter is being properly refreshed when the adapter is being reconstructed. The adapter is being filled with data when the VM gets the object list. As it doesn't have any sense to select any object that is not there yet we just link to the same vm observe method, so we are sure that the objects are selected just when we have the data:

 archerListViewModel.results.observe(this, Observer{
            archersListAdapter.set(it?.toList()!!)
                      fastAdapter.select(archerListViewModel.selectedObjects.map{fastAdapter.getPosition(UUID.fromString(it.uuid).mostSignificantBits)})
            actionModeHelper.checkActionMode(activity as AppCompatActivity)
        })

What is missing, is to transform the SelectedObjects in a live data to be observed instead, this way if an object is deleted (for example) in the db by other user / thread, syn mechanism, the viewmodel could handle it properly and the fragment would update its ui. In order to restore the selection on fastadapter the only way that I found is using the previous code:

fastAdapter.select(archerListViewModel.selectedObjects.map{fastAdapter.getPosition(UUID.fromString(it.uuid).mostSignificantBits)})
            actionModeHelper.checkActionMode(activity as AppCompatActivity)
        })

Maybe the selectionhelperextension could be "extended" to help to do this more "clearly". As a side note, what is happening here is that the actionModeHelper/selectionHelper should be backed their own VM, and use composition to just "plug" the functionallity.

mikepenz commented 6 years ago

Thank you very much for the super detailed answer.

You might want to write an FAQ article too to help others :)