androidkotlinmvvmandroid-livedatadagger-hilt

When i add products to my cart it display nothing


In my MenuFragment I display products that I can then add to the cart, when I click on any product in the logs I see that the product is added to the list and the list is updated, but when I go to CartFragment nothing is displayed, in the logs I see that the observer observes an empty list and passes one to the adapter.

package com.example.restauratio.cart

import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.restauratio.R
import com.example.restauratio.databinding.FragmentCartBinding
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class CartFragment : Fragment() {

    private val cartViewModel: CartViewModel by viewModels()

    private var _binding: FragmentCartBinding? = null
    private val binding get() = _binding!!

    private lateinit var cartAdapter: CartAdapter

    private val actionCartToPop = R.id.action_cartFragment_pop

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

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

        cartAdapter = CartAdapter(cartViewModel)

        binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
        binding.recyclerView.adapter = cartAdapter

        cartViewModel.cartItems.observe(viewLifecycleOwner) { cartItems ->
            Log.d("CartFragment", "Observed cart items: $cartItems")
            cartAdapter.setCartItems(cartItems)
        }

        binding.imageView4.setOnClickListener {
            findNavController().navigate(actionCartToPop)
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
package com.example.restauratio.cart

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.restauratio.menu.DishModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@HiltViewModel
class CartViewModel @Inject constructor() : ViewModel() {

    private val _cartItems = MutableLiveData<List<DishModel>>()
    val cartItems: LiveData<List<DishModel>> get() = _cartItems

    fun addToCart(dish: DishModel) {
        Log.d("CartViewModel", "Adding to cart: $dish")
        val currentItems = _cartItems.value.orEmpty().toMutableList()
        val existingDish = currentItems.find { it.id == dish.id }

        if (existingDish != null) {
            existingDish.quantity += 1
        } else {
            currentItems.add(dish.copy(quantity = 1))
        }

        _cartItems.value = currentItems

        Log.d("CartViewModel", "Updated cart items: ${_cartItems.value}")
    }

    fun removeFromCart(cartItem: DishModel) {
        Log.d("CartViewModel", "Removing from cart: $cartItem")
        val currentCartItems = _cartItems.value.orEmpty().toMutableList()
        currentCartItems.remove(cartItem)
        _cartItems.value = currentCartItems
    }

    fun clearCart() {
        Log.d("CartViewModel", "Clearing cart")
        _cartItems.value = emptyList()
    }

}
package com.example.restauratio.cart

import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import com.example.restauratio.databinding.CartItemBinding
import com.example.restauratio.menu.DishModel
import de.hdodenhof.circleimageview.CircleImageView

class CartAdapter(
    private val cartViewModel: CartViewModel
) : RecyclerView.Adapter<CartAdapter.CartViewHolder>() {

    private var cartItems: List<DishModel> = emptyList()

    inner class CartViewHolder(binding: CartItemBinding) : RecyclerView.ViewHolder(binding.root) {
        val dishImage: CircleImageView = binding.imageView2
        val dishName: TextView = binding.textView2
        val dishPrice: TextView = binding.textView
        val quantityTextView: TextView = binding.textView55
        val removeFromCartButton: ImageView = binding.imageView8

        init {
            removeFromCartButton.setOnClickListener {
                val removedDish = cartItems[adapterPosition]
                cartViewModel.removeFromCart(removedDish)
                Toast.makeText(itemView.context, "Danie usunięte z koszyka", Toast.LENGTH_SHORT).show()
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val cartItemBinding = CartItemBinding.inflate(inflater, parent, false)
        return CartViewHolder(cartItemBinding)
    }

    override fun onBindViewHolder(holder: CartViewHolder, position: Int) {
        val cartItem = cartItems[position]

        holder.dishName.text = cartItem.name
        holder.dishPrice.text = String.format("%.2f zł", cartItem.price)
        holder.quantityTextView.text = cartItem.quantity.toString()
    }

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

    fun setCartItems(newCartItems: List<DishModel>) {
        Log.d("CartAdapter", "Setting cart items: $cartItems")
        cartItems = newCartItems
        notifyDataSetChanged()
    }
}
package com.example.restauratio.menu

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.core.view.GravityCompat
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.restauratio.R
import com.example.restauratio.cart.CartFragment
import com.example.restauratio.cart.CartViewModel
import com.example.restauratio.databinding.FragmentMenuBinding
import com.example.restauratio.loginSession.SessionManager
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class MenuFragment : Fragment() {

    @Inject
    lateinit var sessionManager: SessionManager

    private val menuViewModel: MenuViewModel by viewModels()
    private val cartViewModel: CartViewModel by viewModels()

    private var _binding: FragmentMenuBinding? = null
    private val binding get() = _binding!!

    private lateinit var dishAdapter: DishAdapter

    private val actionMenuToAboutUs = R.id.action_menu_to_aboutUs
    private val actionMenuToReservation = R.id.action_menu_to_reservationView
    private val actionMenuToRules = R.id.action_menu_to_rulesView
    private val actionMenuToPrivacyPolicy = R.id.action_menu_to_privacyPolicy
    private val actionLogout = R.id.action_menu_pop
    private val actionMenuToAlerts = R.id.action_menu_to_alerts
    private val actionMenuToProfile = R.id.action_menu_to_profile
    private val actionMenuToOrders = R.id.action_menu_to_orders
    private val actionMenuToCart = R.id.action_menu_to_cartFragment

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentMenuBinding.inflate(inflater, container, false)

        val drawerLayout = binding.drawerLayout
        val mainView: View = binding.root

        mainView.setOnClickListener {
            if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
                drawerLayout.closeDrawer(GravityCompat.START)
            }
        }

        binding.hamburgerButton.setOnClickListener {
            onHamburgerButtonClick()
        }
        binding.imageView6.setOnClickListener{
            findNavController().navigate(actionMenuToAlerts)
        }
        binding.imageView2.setOnClickListener{
            findNavController().navigate(actionMenuToProfile)
        }
        binding.imageView7.setOnClickListener{
            findNavController().navigate(actionMenuToOrders)
        }
        binding.imageView.setOnClickListener {
            findNavController().navigate(actionMenuToCart)
        }

        binding.navigationView.setNavigationItemSelectedListener { menuItem ->
            when (menuItem.itemId) {
                R.id.about_us -> {
                    findNavController().navigate(actionMenuToAboutUs)
                }
                R.id.reservation -> {
                    findNavController().navigate(actionMenuToReservation)
                }
                R.id.rules -> {
                    findNavController().navigate(actionMenuToRules)
                }
                R.id.privacy_policy -> {
                    findNavController().navigate(actionMenuToPrivacyPolicy)
                }
                R.id.logout -> {
                    sessionManager.logout()
                    findNavController().navigate(actionLogout)
                }
            }
            binding.drawerLayout.closeDrawer(GravityCompat.START)
            true
        }

        requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, onBackPressedCallback)

        return binding.root
    }

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

        dishAdapter = DishAdapter(cartViewModel)

        binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
        binding.recyclerView.adapter = dishAdapter

        menuViewModel.dishes.observe(viewLifecycleOwner) { dishes ->
            dishAdapter.setDishes(dishes)
        }
        menuViewModel.loadDishes(categoryId = null, name = null)

        binding.textInputLayout3.editText?.addTextChangedListener { text ->
            val searchQuery = text.toString().trim()
            menuViewModel.loadDishes(categoryId = null, name = null, searchQuery = searchQuery)
        }
    }

    private fun onHamburgerButtonClick() {
        val drawerLayout = binding.drawerLayout

        if (!drawerLayout.isDrawerOpen(GravityCompat.START)) {
            drawerLayout.openDrawer(GravityCompat.START)
        } else {
            drawerLayout.closeDrawer(GravityCompat.START)
        }
    }

    private val onBackPressedCallback = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            if (sessionManager.isLoggedIn()) {
                requireActivity().finish()
            } else {
                findNavController().popBackStack()
            }
        }
    }

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

package com.example.restauratio.menu

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.restauratio.request.AuthService
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class MenuViewModel @Inject constructor(
    private val authService: AuthService
) : ViewModel() {

    private val _dishes = MutableLiveData<List<DishModel>>()
    val dishes: LiveData<List<DishModel>> get() = _dishes

    fun loadDishes(
        categoryId: Int?,
        name: String?,
        searchQuery: String? = null
    ) {
        viewModelScope.launch {
            try {
                val dishesResponse = authService.getDishes(DishRequest(categoryId, name))
                if (dishesResponse.isSuccessful) {
                    val allDishes = dishesResponse.body()?.dishes ?: emptyList()

                    val filteredDishes = if (!searchQuery.isNullOrBlank()) {
                        allDishes.filter { dish -> dish.name.contains(searchQuery, ignoreCase = true) }
                    } else {
                        allDishes
                    }

                    _dishes.value = filteredDishes
                } else {
                    Log.e("MenuViewModel", "Failed to fetch dishes. Error code: ${dishesResponse.code()}")
                }
            } catch (e: Exception) {
                Log.e("MenuViewModel", "An error occurred while fetching dishes", e)
            }
        }
    }
}
package com.example.restauratio.menu

import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import com.example.restauratio.cart.CartViewModel
import com.example.restauratio.databinding.MenuItemBinding
import de.hdodenhof.circleimageview.CircleImageView

class DishAdapter(
    private val cartViewModel: CartViewModel
) : RecyclerView.Adapter<DishAdapter.DishViewHolder>() {

    private var dishes: List<DishModel> = emptyList()

    inner class DishViewHolder(binding: MenuItemBinding) : RecyclerView.ViewHolder(binding.root) {
        val dishImage: CircleImageView = binding.imageView2
        val dishName: TextView = binding.textView2
        val dishPrice: TextView = binding.textView
        val addToCartButton: ImageView = binding.imageView8

        init {
            addToCartButton.setOnClickListener {
                val clickedDish = dishes[adapterPosition]
                cartViewModel.addToCart(clickedDish)
                Toast.makeText(itemView.context, "Danie dodane do koszyka", Toast.LENGTH_SHORT).show()
            }
        }

    }


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DishViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val menuItemBinding = MenuItemBinding.inflate(inflater, parent, false)
        return DishViewHolder(menuItemBinding)
    }

    override fun onBindViewHolder(holder: DishViewHolder, position: Int) {
        val dish = dishes[position]

        holder.dishName.text = dish.name
        holder.dishPrice.text = String.format("%.2f zł", dish.price)

    }

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

    fun setDishes(newDishes: List<DishModel>) {
        dishes = newDishes
        notifyDataSetChanged()
    }
}
package com.example.restauratio.menu

data class DishModel(
    val id: Int,
    val name: String,
    val description: String,
    val price: Double,
    val dishCategoryIds: List<Int>,
    var quantity: Int = 0
)

data class DishRequest(
    val categoryId: Int?,
    val name: String?
)

data class DishResponse(
    val dishes: List<DishModel>,
    val total: Int
)

has no idea why this is happening


Solution

  • When you switch to CartFragment, a new instance of fragment is created which also creates a new instance of viewmodel. To solve this introduce the concept of sharedviewmodel by using activityViewModels instead of viewModels.

    activityViewModels is tied to the activity hosting the fragments, while viewModels is tied to each fragment. Below is a sample of how your code should look:

    CartFragment

    @AndroidEntryPoint
    class CartFragment : Fragment() {
    
        private val cartViewModel: CartViewModel by activityViewModels()
    ....
    }
    
    

    MenuFragment

    @AndroidEntryPoint
    class MenuFragment : Fragment() {
    ...
        private val cartViewModel: CartViewModel by activityViewModels()
    ...
    

    To understand more, check on this codelab - Shared viewmodels across fragments

    Another option incase you are saving the data to db or api, then add a new function to just get the data and update the observable then call the function on the CartFragment onviewCreated.