Home > Enterprise >  MutableLiveData is not posting the correct value - Android
MutableLiveData is not posting the correct value - Android

Time:11-02

I'm building an app around a Meals API. First a list of categories will be shown. On selecting a category, meals of the particular category will be displayed. Users can add any meal to favorites. User can see the list of favorites by clicking on the favorites icon at the top. If a user clicks a meal, he gets to see the details of the recipe for that meal. I'm using the same fragment & viewmodel to display the meals based on the category and also the favorite meals. I'm using MVVM with MutableLiveData. The favorites icon is in the activity and I keep on replacing the fragments within the activity.

If I click on the favorites icon from the CategoriesFragment or the FilterByTypeFragment, all the meals added to the favorites are shown correctly. But if I click on the favorites icon when I'm in the RecipeDetailsFragment, it just displays all the meals of that category. That means, it just works like a back press on the RecipeDetailsFragment. When I debug, I see that the fetchFavorites method is being called in the viewmodel when I click on the favorites icon. But what the observer gets at the end is the list of all the meals of the selected category. Why don't I see only the list of favorites when I navigate from RecipeDetailsFragment to the favorites section? I'm posting the code below for your understanding:

    @HiltViewModel
    class FilterByCategoryViewModel @Inject constructor(
        val dataManager: AppDataManager,
        val networkHelper: NetworkHelper,
        val category: String?,
        val isFavorites: String = "N"
    ) : ViewModel() {
    
        val _meals = MutableLiveData<Resource<MealsResponse>>()
    
        init {
            if (isFavorites.equals("Y")) fetchFavorites()
            else fetchMealsByCategory(category)
        }
    
        fun fetchMealsByCategory(category: String?) {
            viewModelScope.launch {
                _meals.postValue(Resource.loading(null))
                if (networkHelper.isNetworkConnected()) {
                    launch(Dispatchers.IO) {
                        dataManager.getMealsByCategory(category!!).let {
                            if (it.isSuccessful) {
                                _meals.postValue(Resource.success(it.body()))
                            } else _meals.postValue(
                                Resource.error(
                                    it.errorBody().toString(),
                                    null
                                )
                            )
                        }
                    }
                } else _meals.postValue(Resource.error("No Internet Connection", null))
            }
        }
    
        fun fetchFavorites() {
            viewModelScope.launch {
                _meals.postValue(Resource.loading(null))
                if (networkHelper.isNetworkConnected()) {
                    launch(Dispatchers.IO) {
                        dataManager.getFavoriteMeals().let {
                            if (it.isSuccessful) {
                                                            println("Body: "   it.body().toString())
_meals.postValue(Resource.success(it.body()))
                                println(_meals.getValue().toString())
                            } else _meals.postValue(
                                Resource.error(
                                    it.errorBody().toString(),
                                    null
                                )
                            )
                        }
                    }
                } else _meals.postValue(Resource.error("No Internet Connection", null))
            }
        }
    
    
        fun onFavoriteClicked(meal: Meal) {
            viewModelScope.launch {
                val job = launch(Dispatchers.IO) {
                    val isFavorite = dataManager.isFavorite(meal)
                    val _meal = meal.copy(
                        isFavorite = when (isFavorite) {
                            1 -> 0
                            else -> 1
                        }
                    )
                    dataManager.setFavorite(_meal)
                }
                job.join()
                if (isFavorites.equals("Y"))
                    fetchFavorites()
                else
                    fetchMealsByCategory(category)
            }
        }
    }

The Fragment:

@AndroidEntryPoint
class FilterByTypeFragment : Fragment(), MealAdapter.FavoriteClickListener {

    @Inject
    lateinit var dataManager: AppDataManager

    @Inject
    lateinit var networkHelper: NetworkHelper
    private lateinit var adapter: MealAdapter
    private lateinit var binding: FragmentCategoriesBinding
    private var category: String? = null
    private var isFavorites: String = "N"
    lateinit private var filterByCategoryViewModel: FilterByCategoryViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate<FragmentCategoriesBinding>(
            inflater,
            R.layout.fragment_categories, container, false
        )
        return binding.root
    }

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

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        val controller: NavController = Navigation.findNavController(view!!)
        controller.popBackStack(R.id.recipeDetailFragment, true)
        if (arguments != null) {
            category = FilterByTypeFragmentArgs.fromBundle(requireArguments()).category
            isFavorites = FilterByTypeFragmentArgs.fromBundle(requireArguments()).isFavorites
        }
        setupUI()
    }

    private fun setupUI() {
        binding.recyclerView.layoutManager = GridLayoutManager(activity, 2)
        adapter = MealAdapter(arrayListOf(), this)
        binding.recyclerView.addItemDecoration(
            GridSpacingItemDecoration(true, 2, 20, true)
        )
        binding.recyclerView.adapter = adapter
    }

    override fun onResume() {
        super.onResume()

        val factory = FilterByTypeViewModelFactory(dataManager, networkHelper, category, isFavorites)
        filterByCategoryViewModel = ViewModelProvider(
            this,
            factory
        ).get(FilterByCategoryViewModel::class.java)
        setupObserver()
    }

    private fun setupObserver() {
        filterByCategoryViewModel._meals.observe(this, Observer {
            when (it.status) {
                Status.SUCCESS -> {
                    binding.progressBar.visibility = View.GONE
                    it.data?.let { users ->
                        renderList(users.meals)
                    }
                    binding.recyclerView.visibility = View.VISIBLE
                }
                Status.LOADING -> {
                    binding.progressBar.visibility = View.VISIBLE
                    binding.recyclerView.visibility = View.GONE
                }
                Status.ERROR -> {
                    //Handle Error
                    binding.progressBar.visibility = View.GONE
                    Toast.makeText(activity, it.message, Toast.LENGTH_LONG).show()
                }
            }
        })
    }

    private fun renderList(meals: List<Meal>) {
        println("Meals: "   meals.toString())
        adapter.clearData()
        adapter.addData(meals)
        adapter.notifyDataSetChanged()
    }

    override fun onFavoriteClick(meal: Meal) {
        filterByCategoryViewModel.onFavoriteClicked(meal)
    }
}

CodePudding user response:

Writing down the discussion in comments in form of a proper answer:
It turned out that the problem was that you were navigating to a destination which was already there in the back stack, and since there was already a ViewModel scoped to that fragment, you received the same instance of viewModel (which already had a isFavorites field in its constructor.

The problem got solved by removing the old destination from backstack before navigating so that we get a new ViewModel instance.

Adding to that, I would suggest not putting such fields (which are business logic dependent) as a dependency to view models (in your case category and isFavorites). Instead you can adopt some other approaches like:

  • Pass category and isFavorites as navigation arguments and retrieve them inside ViewModel using SavedStateHandle OR
  • Represent these fields as LiveData or Flow in your ViewModel and react to changes in these values.
  • Related