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
)