androidkotlinandroid-recyclerviewandroid-button

Kotlin: text disappears from buttons in RecyclerView


I set up a RecyclerView that contains a list of buttons. The length of the list and the text for each button should come from the results of a db query (where I look for all recipes saved in the db).

Somehow, I see the correct text appearing on the buttons for a fraction of a second when I open the fragment, but it disappears immediately after, leaving a list of blank buttons. What am I doing wrong?

Here is the RecipeListAdapter.kt:

package com.example.recipy

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.recipy.databinding.RecipeRowViewBinding

class RecipeListAdapter(private val recipesList: ArrayList<RecipeRowViewModel>) : RecyclerView.Adapter<RecipeListAdapter.RecipeListViewHolder>() {

    inner class RecipeListViewHolder(private val binding : RecipeRowViewBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(recipeRow : RecipeRowViewModel){
            binding.recipeNameButton.text = recipeRow.recipe
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeListViewHolder =
        RecipeListViewHolder(RecipeRowViewBinding.inflate(LayoutInflater.from(parent.context), parent, false))

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

    override fun getItemCount(): Int {
        return recipesList.size
    }
}

the RecipeRowViewModel.kt:

package com.example.recipy

data class RecipeRowViewModel(var recipe : String)

the recipe_row_view.xml:

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="recipeRow"
            type="com.example.recipy.RecipeRowViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/recipe_name_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="5dp"
            android:text="@={recipeRow.recipe}">
        </Button>

    </LinearLayout>

</layout>

the BrowseRecipeFragment that contains the RecyclerView:

package com.example.recipy

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.recipy.databinding.FragmentBrowseRecipesBinding

class BrowseRecipesFragment : Fragment() {

    private var _binding: FragmentBrowseRecipesBinding? = null

    private val binding get() = _binding!!

    private val recipesList : ArrayList<RecipeRowViewModel> = arrayListOf()

    private lateinit var customAdapter : RecipeListAdapter

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentBrowseRecipesBinding.inflate(inflater, container, false)
        setAdapter()
        return binding.root
    }


    private fun setAdapter(){
        customAdapter = RecipeListAdapter(recipesList)
        binding.recipeRowRecyclerView.apply {
            layoutManager = LinearLayoutManager(requireContext())
            adapter = customAdapter
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Retrieve recipes from db and add them to recipesList
        val db = DBHelper(requireContext(), null)
        val cursor = db.getAllRecipes()
        cursor!!.moveToFirst()
        recipesList.add(RecipeRowViewModel(cursor.getString(cursor.getColumnIndex(DBHelper.RECIPE_NAME_COL))))
        while(cursor.moveToNext()){
            recipesList.add(RecipeRowViewModel(cursor.getString(cursor.getColumnIndex(DBHelper.RECIPE_NAME_COL))))
        }
        cursor.close()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

and fragment_browse_recipes.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".BrowseRecipesFragment">

    <TextView
        android:id="@+id/textview_second"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Here will be the search bar"
        android:textSize="20sp"
        app:layout_constraintTop_toTopOf="parent" />

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textview_second" >

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recipe_row_recycler_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            tools:listitem="@layout/recipe_row_view" />
    </ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

Solution

  • You have bug in your data binding implementation. In your recipe_row_view.xml you define recipeRow variable for the layout but you are not setting value to it. The reason why you see it for short time is that you set the text like this binding.recipeNameButton.text = recipeRow.recipe and after that data binding is invalidating the layout, but your recipeRow variable is not set, so text is empty.

    Solution 1

    Using data binding, remove binding.recipeNameButton.text = recipeRow.recipe and set the recipeRow variable. This will automatically set the text when recipeRow variable changes.

    inner class RecipeListViewHolder(private val binding: RecipeRowViewBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(recipeRow: RecipeRowViewModel) {
            //Fix, this will set the layout variable and text gets updated automatically when this changes
            binding.recipeRow = recipeRow
        }
        
        ...
    
    }
    

    Solution 2

    The classic way, remove android:text="@={recipeRow.recipe}" from your recipe_row_view.xml and keep binding.recipeNameButton.text = recipeRow.recipe.