Home > Software design >  Data of addview cannot appeared in linearLayout of scrollview
Data of addview cannot appeared in linearLayout of scrollview

Time:09-06


I am trying to applying tutorial, this a dummy store app, I showing the data that added from user inside view then added it in linear layout of scrollview, the app is run fine and the data is added to viewmodel I see it in the log but I failed to diplay it in the view and I don't know what the problem is

The code of model class

@Parcelize
data class Shoe(
    var name: String, var size: Double, var company: String, var description: String,
    val images: List<String> = mutableListOf()
) : Parcelable

MainViewModel

class MainViewModel : ViewModel() {

    private val _shoesList = MutableLiveData<MutableList<Shoe>>()
    val shoesList: LiveData<MutableList<Shoe>>
        get() = _shoesList

    fun addShoe(shoe: Shoe) {
        _shoesList.value?.add(shoe)
    }

    init {
        Timber.tag(TAG).i(": MainViewModel created")
        val testShoe1 = Shoe("Test Shoe 1",

            47.0, "dummy shoe company", "Dummy shoe description")
        val testShoe2 = Shoe("Test Shoe 2",
            40.0, "dummy shoe company", "Dummy shoe description")


        _shoesList.value?.add(testShoe1)
        _shoesList.value?.add(testShoe2)
    }


}

fragment_shoe_list layout

<?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"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@ id/shoeListLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.ShoeListFragment">


        <ScrollView
            android:id="@ id/scrollView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <LinearLayout
                android:id="@ id/shoeListLinearLayout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical" />
        </ScrollView>

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@ id/floatingActionButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="24dp"
            android:layout_marginBottom="24dp"
            android:clickable="true"
            android:focusable="true"
            android:src="@drawable/ic_baseline_add"
            app:layout_constraintBottom_toBottomOf="@ id/scrollView"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@ id/scrollView"
            app:layout_constraintVertical_bias="1.0"
            android:contentDescription="@string/button_add_shoe" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

ShoeListFragment class

class ShoeListFragment : Fragment() {

    private lateinit var binding: FragmentShoeListBinding

    private val mainViewModel: MainViewModel by activityViewModels()

    private lateinit var showRowBinding: ShoeRowLayoutBinding

    private val TAG = "ShoeListFragment"

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

        binding =
            DataBindingUtil.inflate(layoutInflater, R.layout.fragment_shoe_list, container, false)

        return binding.root
    }

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

        mainViewModel.shoesList.observe(viewLifecycleOwner) {
            it.forEach { shoe ->

                Log.d(TAG, "onViewCreated: ${it.toString()}")
                
                Timber.tag(TAG).i("test shoe ${shoe.name}")

                showRowBinding = ShoeRowLayoutBinding.inflate(layoutInflater)


                showRowBinding.newShoe = shoe

                binding.shoeListLinearLayout.addView(showRowBinding.root)
            }
        }

        binding.floatingActionButton.setOnClickListener {
            Timber.i(mainViewModel.shoesList.value?.joinToString(separator = "\n"))
            findNavController().navigate(ShoeListFragmentDirections.actionShoeListFragmentToDetailsFragment())
        }
    }
}

fragment_details layout

<?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"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="mainViewModel"
            type="com.udacity.shoestore.models.MainViewModel" />

        <variable
            name="shoeData"
            type="com.udacity.shoestore.models.Shoe" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.DetailsFragment">


        <TextView
            android:id="@ id/shoeNameTV"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:text="@string/shoe_name"
            app:layout_constraintBottom_toTopOf="@ id/shoeNameED"
            app:layout_constraintEnd_toEndOf="@ id/shoeNameED"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="@ id/shoeNameED" />

        <TextView
            android:id="@ id/companyNameTV"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:text="Company Name"
            app:layout_constraintBottom_toTopOf="@ id/companyNameED"
            app:layout_constraintEnd_toEndOf="@ id/companyNameED"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="@ id/companyNameED" />

        <TextView
            android:id="@ id/shoeSizeTV"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:text="Shoe Size"
            app:layout_constraintBottom_toTopOf="@ id/shoeSizeED"
            app:layout_constraintEnd_toEndOf="@ id/shoeSizeED"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="@ id/shoeSizeED" />


        <TextView
            android:id="@ id/descTV"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:text="Description"
            app:layout_constraintBottom_toTopOf="@ id/descriptionED"
            app:layout_constraintEnd_toEndOf="@ id/descriptionED"
            app:layout_constraintHorizontal_bias="0.007"
            app:layout_constraintStart_toStartOf="@ id/descriptionED" />

        <Button
            android:id="@ id/cancelButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:text="Cancel"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@ id/saveButton" />

        <Button
            android:id="@ id/saveButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="136dp"
            android:text="Save"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@ id/shoeSizeED" />

        <EditText
            android:id="@ id/shoeNameED"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="100dp"
            android:ems="10"
            android:inputType="textPersonName"
            android:text="@={shoeData.name}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.497"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <EditText
            android:id="@ id/descriptionED"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="32dp"
            android:ems="10"
            android:text="@={shoeData.description}"
            android:inputType="textPersonName"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.497"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@ id/companyNameED" />


        <EditText
            android:id="@ id/companyNameED"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="32dp"
            android:ems="10"
            android:text="@={shoeData.company}"
            android:inputType="textPersonName"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.497"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@ id/shoeNameED" />


        <EditText
            android:id="@ id/shoeSizeED"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="28dp"
            android:ems="10"
            android:text="@={``   shoeData.size}"
            android:inputType="numberDecimal"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.497"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@ id/descriptionED" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

FragmentDetails Class

class DetailsFragment : Fragment() {

    private lateinit var binding: FragmentDetailsBinding

    private val mainViewModel: MainViewModel by activityViewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Inflate the layout for this fragment

        binding =
            DataBindingUtil.inflate(layoutInflater, R.layout.fragment_details, container, false)

        binding.mainViewModel = mainViewModel
        binding.lifecycleOwner = this


        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
//        binding.mainViewModel = this.mainViewModel

        binding.saveButton.setOnClickListener {

            if (TextUtils.isEmpty(binding.shoeNameED.text.toString())) {
                binding.shoeNameED.error = "required text"
            } else if (TextUtils.isEmpty(binding.shoeSizeED.text.toString())) {
                binding.shoeSizeED.error = "required text"
            } else if (TextUtils.isEmpty(binding.companyNameED.text.toString())) {
                binding.companyNameED.error = "required text"
            } else if (TextUtils.isEmpty(binding.descriptionED.text.toString())) {
                binding.descriptionED.error = "required text"
            } else {

                val newShoe = Shoe(
                    name = binding.shoeNameED.text.toString(),
                    size = binding.shoeSizeED.text.toString().toDouble(),
                    company = binding.companyNameED.text.toString(),
                    description = binding.descriptionED.text.toString()
                )
                binding.shoeData = newShoe

                Timber.tag(TAG).i(newShoe.toString())

                mainViewModel.addShoe(newShoe)

                findNavController().navigate(DetailsFragmentDirections.actionDetailsFragmentToShoeListFragment())
            }

        }

    }
}

and finally this a row layout that should inflated and added to the scrollView

<?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="newShoe"
            type="com.udacity.shoestore.models.Shoe" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="20dp">

        <TextView
            android:id="@ id/company_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Company: "
            android:textColor="@android:color/black"
            android:textStyle="bold"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@ id/name_label" />

        <TextView
            android:id="@ id/name_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Name: "
            android:textColor="@android:color/black"
            android:textStyle="bold"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />


        <TextView
            android:id="@ id/size_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Size: "
            android:textColor="@android:color/black"
            android:textStyle="bold"
            app:layout_constraintStart_toStartOf="@ id/company_label"
            app:layout_constraintTop_toBottomOf="@ id/company_label" />

        <TextView
            android:id="@ id/desc_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Description: "
            android:textColor="@android:color/black"
            android:textStyle="bold"
            app:layout_constraintStart_toStartOf="@ id/size_label"
            app:layout_constraintTop_toBottomOf="@ id/size_label" />

        <TextView
            android:id="@ id/desc_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:text="@{newShoe.description}"
            app:layout_constraintStart_toEndOf="@ id/desc_label"
            app:layout_constraintTop_toBottomOf="@ id/size_text" />


        <TextView
            android:id="@ id/size_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:text="@{Double.toString(newShoe.size)}"
            app:layout_constraintStart_toEndOf="@ id/size_label"
            app:layout_constraintTop_toBottomOf="@ id/company_text" />

        <TextView
            android:id="@ id/name_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:text="@{newShoe.name}"
            app:layout_constraintStart_toEndOf="@ id/name_label"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@ id/company_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:text="@{newShoe.company}"
            app:layout_constraintStart_toEndOf="@ id/company_label"
            app:layout_constraintTop_toBottomOf="@ id/name_text" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

CodePudding user response:

There are a few potential issues I can see, I don't know which is causing the problem here, but you can take a look at both!

Firstly, when you add an item in your ViewModel, you're doing this:

fun addShoe(shoe: Shoe) {
    _shoesList.value?.add(shoe)
}

But your LiveData doesn't contain an initial value:

private val _shoesList = MutableLiveData<MutableList<Shoe>>()

So there's no value, no list to add to. Since you're null-checking value that line is going to fail silently. You said you're adding shoes to the VM, and you can see them in the log, but the code you posted won't do that.


The second issue is that even if you have an initial empty list, what that addShoe function does is get a reference to the current List held in that LiveData, and then changing its contents. The LiveData itself doesn't know anything has changed, so it won't push an update to its observers. Depending on what's going on with your Fragments, if they're not being recreated it's possible they're just not seeing any observer updates, so they're not displaying anything new.

You can fix that by setting value on the LiveData again. You can just hand it the current value, it's the setting that's important. That way it will notify all the observers:

// create an empty list as the initial value
private val _shoesList = MutableLiveData<MutableList<Shoe>>(mutableListOf())

fun addShoe(shoe: Shoe) {
    // one of the few places I recommend using !!, when you know you'll always have a value set
    val list = _shoesList.value!!.add(shoe)
    // or you could do
    // val list = (_shoesList.value ?: mutableListOf<Shoe>()).add(shoe)
    _shoesList.value = list
}

But honestly, using a MutableList can cause problems, because the data can change silently in the background as the list is modified - if any of your observers are using the last list value you passed (which is actually the same list) then you're changing the data they hold, and that can break things that compare the "new list" you pass to the "old list" - changes disappear because they're identical, because they're the same object.

It's cleaner to just use immutable lists:

// use an immutable List, with an empty version as the start value
private val _shoesList = MutableLiveData<List<Shoe>>(emptyList())

fun addShoe(shoe: Shoe) {
    // updating is as simple as making a new list
    _shoesList.value = _shoesList.value!!   shoe
}

and that takes care of your updating, pushing a new value, and since it's a new list every time it won't affect the old lists you pushed earlier (unless you edit the Shoe objects themselves of course)


And lastly if you're going to be adding view to your LinearLayout, you probably want to use the inflate method that takes the parent View, so it can be inflated to the correct size (e.g. it needs to know what size match_parent actually works out as):

showRowBinding = ShoeRowLayoutBinding.inflate(layoutInflater, binding.shoeListLinearLayout, false)

Or you could try making that last parameter true to attach it to that parent directly (which you're doing with addView)

  • Related