3

I am attempting to set up a recycler view, with the elements being displayed by it using a ConstraintLayout. I used the layout from this example by Google as a guideline.

However, despite specifying android:layout_width="match_parent" all the way down, the result being finally displayed on screen is more akin to wrap_content.

What I have tried so far

  1. Changing the ConstraintLayout to a LinearLayout
  2. The workaround suggested in this stackoverflow question
  3. Setting android:layout_width=0, while using a ConstraintLayout and anchoring start, end and top to the parent
  4. Wrapping the whole thing inside a CardView
  5. Running the app on multiple devices, both emulated and physical

Relevant code

If anything else of interest is missing, please let me know.

recipe_list_entry.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="@dimen/recipe_entry_height" >

    <TextView
        android:id="@+id/recipe_title"
        style="@style/Widget.RecipeTracker.ListItemTextView"
        android:layout_marginHorizontal="@dimen/margin_between_elements"
        android:background="@color/design_default_color_on_secondary"
        android:layout_width="match_parent"
        android:fontFamily="sans-serif"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:maxLines="1"
        android:ellipsize="end"
        tools:text="amazing stuff"/>
</androidx.constraintlayout.widget.ConstraintLayout>

styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Widget.RecipeTracker.ListItemTextView" parent="Widget.MaterialComponents.TextView">
        <item name="android:gravity">center_vertical</item>
        <item name="android:layout_height">48dp</item>
        <item name="android:textAppearance">?attr/textAppearanceBody1</item>
    </style>
</resources>

recipe_list_fragment.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=".RecipeListFragment">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="@dimen/margin_between_elements"
        android:scrollbars="vertical"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/add_entry"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_marginEnd="@dimen/add_entry_fab_margin"
        android:layout_marginBottom="@dimen/add_entry_fab_margin"
        android:contentDescription="@string/add_new_recipe_contentDescription"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:srcCompat="@drawable/ic_add" />

</androidx.constraintlayout.widget.ConstraintLayout>

RecipeListFragment.kt

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import me.lindenbauer.recipetracker.databinding.RecipeListFragmentBinding

/**
 * A simple [Fragment] subclass as the default destination in the navigation.
 */
class RecipeListFragment : Fragment() {

    private val viewModel: RecipeTrackerViewModel by activityViewModels {
        RecipeTrackerViewModelFactory(
            (activity?.application as RecipeTrackerApplication).database.recipeDao()
        )
    }

    private var _binding: RecipeListFragmentBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        _binding = RecipeListFragmentBinding.inflate(inflater, container, false)
        return binding.root

    }

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

        // Set up the automatic data updates for the recyclerView using the RecipeListAdapter
        val adapter = RecipeListAdapter {
            // TODO: Define onRecipeClicked here
        }
        // This specifies how the "cards" are displayed by the recyclerView
        binding.recyclerView.layoutManager = LinearLayoutManager(this.context)
        binding.recyclerView.adapter = adapter

        // Attach an observer on the allItems list to update the UI automatically when the data
        // changes.
        viewModel.allRecipes.observe(this.viewLifecycleOwner) { recipes ->
            recipes.let{ adapter.submitList(it) }
        }

        binding.addEntry.setOnClickListener{
            findNavController().navigate(R.id.action_RecipeListFragment_to_AddEntryFragment)
        }
    }

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

RecipeListAdapter.kt

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import me.lindenbauer.recipetracker.data.Recipe
import me.lindenbauer.recipetracker.databinding.RecipeListEntryBinding

class RecipeListAdapter(private val onRecipeClicked: (Recipe) -> Unit):
    ListAdapter<Recipe, RecipeListAdapter.RecipeViewHolder>(DiffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeViewHolder {
        return RecipeViewHolder(
            RecipeListEntryBinding.inflate(
                LayoutInflater.from(
                    parent.context
                )
            )
        )
    }

    override fun onBindViewHolder(holder: RecipeViewHolder, position: Int) {
        // Set up anything that is supposed to happen when interacting with a single item in the recyclerView
        val current = getItem(position)
        holder.itemView.setOnClickListener {
            onRecipeClicked(current)
        }

        holder.bind(current)
    }

    class RecipeViewHolder(private var binding: RecipeListEntryBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(recipe: Recipe) {
            binding.recipeTitle.text = recipe.recipeTitle
        }
    }


    companion object {
        private val DiffCallback = object : DiffUtil.ItemCallback<Recipe>() {
            override fun areItemsTheSame(oldRecipe: Recipe, newRecipe: Recipe): Boolean {
                return oldRecipe === newRecipe
            }

            override fun areContentsTheSame(oldRecipe: Recipe, newRecipe: Recipe): Boolean {
                // TODO: Might need to make this more sophisticated
                return oldRecipe.recipeTitle == newRecipe.recipeTitle
            }
        }
    }
}

Designer preview

Preview from Android Studio designer Preview from Android Studio designer

Actual result

Actual result on emulated Pixel 5 running API 30/Android 11 Actual result on emulated Pixel 5 running API 30/Android 11

Liqs
  • 137
  • 1
  • 9

1 Answers1

12

Your code here:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeViewHolder {
    return RecipeViewHolder(
        RecipeListEntryBinding.inflate(
            LayoutInflater.from(
                parent.context
            )
        )
    )
}

does not inflate the view in the context of the parent view, so some layout parameters will not be treated as you expect. In this case, it can't have a width of match_parent if it doesn't know who its parent is. Change it to:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeViewHolder {
    return RecipeViewHolder(
        RecipeListEntryBinding.inflate(
            LayoutInflater.from(
                parent.context
            ),
            parent,
            false
        )
    )
}

Also, a tip. You don't need to create a companion object just to hold your DiffCallback. And you don't need a property to hold an object either. For cleaner code, instead of

companion object {
    private val DiffCallback = object : DiffUtil.ItemCallback<Recipe>() {
        //...
    }
}

you could put

private object DiffCallback: DiffUtil.ItemCallback<Recipe>() {
    //...
}
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Thank you, that seemed to have done the trick. Just for my understanding though, as I am learning native Android right now: [The problem was this usecase correct?](https://developer.android.com/topic/libraries/view-binding#fragments) Could you maybe point me to someplace where the methods generated by Binding classes are documented? The method isn't even linked in the Android documentation that I linked in this comment. I was not able to find anything documenting the available overrides for ViewBinding.inflate(). It would probably help me understand ViewBinding some more. – Liqs Nov 16 '21 at 08:53
  • 1
    I haven’t found any way to look at specific documentation for the binding methods, but the two overloads of `ViewBinding.inflate` match up with overloads of `LayoutInflater.inflate` except the first parameter of the view ID is swapped out for the LayoutInflater itself. There is also `ViewBinding.bind()` that creates a binding instance for a view that has already been inflated. That’s especially useful for Fragments where you have passed a layout ID to the super constructor. You can omit the entire `onCreateView` boilerplate and bind your binding in `onViewCreated` instead. – Tenfour04 Nov 16 '21 at 12:56
  • thanks so much, spent couple of hours trying to figure this out! – shtas Mar 08 '23 at 14:05