Search code examples
androidandroid-recyclerviewkotlinandroid-databindingandroid-viewmodel

No Data with RecyclerView, Data Binding and ViewModel


New to Android development and trying to wrap my head around the latest Architecture Components. Using Android Studio 3.2, Room, LiveData, ViewModel, Data Binding and RecyclerView and I’ve been fighting with Data Binding for days. I have a fragment in my activity and the RecyclerView portion that should be showing data from my ViewModel/Query is blank/empty. The Log.d in getItemCount for the Adapter shows zero items even though the related Room query is valid and returns items.

Please hit me with the clue stick and let me know what I am missing. The relevant portions of my code:

Entity and Dao

import org.threeten.bp.Instant

data class ActionDetails(val time: Instant,
                     val firstName: String,
                     ... )

@Query("SELECT time, first_name as firstName...")
fun liveStatus(): LiveData<List<ActionDetails>>

Fragment Class

class MainFragment : Fragment() {
...
private lateinit var viewModel: MainViewModel
private lateinit var adapter: ViewAdapter

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View {
    val binding = MainFragmentBinding.inflate(inflater, container, false)
    val context = context ?: return binding.root

    val factory = Utilities.provideMainViewModelFactory(context)
    viewModel = ViewModelProviders.of(this, factory).get(MainViewModel::class.java)

    adapter = ViewAdapter(listOf())

    binding.apply {
        rvActionDetails.setHasFixedSize(true)
        rvActionDetails.layoutManager = LinearLayoutManager(context)
        rvActionDetails.adapter = adapter
        vm = viewModel
        setLifecycleOwner(this@MainFragment)
    }

    return binding.root
}

ViewModel

class MainViewModel(private val repository: DataRepository) : ViewModel() {
    val actions: LiveData<List<ActionDetails>> = repository.liveStatus()
}

Adapter

import ...FragmentActionDetailBinding

class ViewAdapter(private val actions: List<ActionDetails>) : RecyclerView.Adapter<ViewAdapter.ViewHolder>() {
private val TAG = this::class.java.simpleName

class ViewHolder(val binding: FragmentActionDetailBinding) : RecyclerView.ViewHolder(binding.root) {
    fun bind(action: ActionDetails) {
            binding.apply {
            vm = action
            executePendingBindings()
            }
    }
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val inflater = LayoutInflater.from(parent.context)
    val binding = FragmentActionDetailBinding.inflate(inflater, parent, false)
    return ViewHolder(binding)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.bind(actions[position])
}

override fun getItemCount(): Int {
    Log.d(TAG, "Adapter has ${actions.size} items!")
    return actions.size
}
}

Main Fragment

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<data>
    <variable
        name="vm" type=".MainViewModel" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainFragment" >
    ...
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_action_details"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        tools:listitem="@layout/fragment_action_detail" />

</androidx.constraintlayout.widget.ConstraintLayout>

Fragment XML

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<data>
    <variable
        name="vm" type=".ActionDetails" />
</data>

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:context=".ActionDetailFragment" >

    <TextView
        android:id="@+id/first_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{vm.firstName}"
        tools:text="John"/>
    ...
</LinearLayout>


Solution

  • Finally resolved this issue. Not sure exactly what resolved it—I think it was due to moving the ViewModel and running executePendingBindings() prior to configuring the RecyclerView in binding.apply in the fragment, but I’m not certain—because I also made some other changes after learning about BindingAdapters.

    My changes are below if it helps anyone else—I’d also appreciate any comments on how this could be improved.

    Cheers.

    Fragment Class

    class MainFragment : Fragment() {
    ...
    
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View {
    ...
        binding.apply {
            //Need to do this before configuring the RecyclerView
            vm = viewModel
            setLifecycleOwner(this@MainFragment)
            executePendingBindings()
    
            rvActionDetails.setHasFixedSize(true)
            rvActionDetails.layoutManager = LinearLayoutManager(context)
            rvActionDetails.adapter = adapter
        }
    
        return binding.root
    }
    

    Adapter

    import ...FragmentActionDetailBinding
    
    class ViewAdapter(private val actions: List<ActionDetails>) : RecyclerView.Adapter<ViewAdapter.ViewHolder>() {
    ...
        override fun setData(data: List<ActionDetails>) {
            actions = data
            notifyDataSetChanged()
    }
    

    Binding Adapter

    @BindingAdapter("listData")
    fun <T> setRecyclerViewList (recyclerView: RecyclerView, data: T) {
        if (recyclerView.adapter is BindableListAdapter<*>) {
            (recyclerView.adapter as BindableListAdapter<T>).setData(data)
        }
    }
    
    interface BindableListAdapter<T> {
        fun setData(data: T)
    }
    

    Main Fragment

    <layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    
    <data>
        <variable
            name="vm" type=".MainViewModel" />
    </data>
    
    <androidx.constraintlayout.widget.ConstraintLayout
        ...
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_action_details"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:listData="@{vm.actions}"
            tools:listitem="@layout/fragment_action_detail" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    Fragment XML

    <layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    
    <data>
        <variable
            name="vm" type=".ActionDetails" />
    </data>
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:context=".ActionDetailFragment" >
    
        <TextView
            android:id="@+id/first_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{vm.firstName}"
            tools:text="John"/>
        ...
    </LinearLayout>