0

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)
    }
}
Neeraja Gandla
  • 97
  • 1
  • 6
  • 17
  • You said that `fetchFavorites` is getting called. Inside the function you have this statement `println(_meals.getValue().toString())`. Is this printing the right thing (i.e. list of all favorites)? – Arpit Shukla Oct 30 '21 at 07:26
  • No it prints all the meals of the category. Now I have added another `println` statement just before `_meals.postValue` . That one prints only the favorites. But the println after `_meals.postValue` prints all the meals of the category. – Neeraja Gandla Oct 30 '21 at 09:22
  • Probably this is because the value set using `postValue` is not set immediately as you are in a different thread. To debug this, add a `delay(100)` statment after `postValue`. Then you will be able to print the right value. (Remember to remove that `delay` call later) – Arpit Shukla Oct 30 '21 at 09:42
  • Are you using this same viewmodel for all the fragments? – Arpit Shukla Oct 30 '21 at 09:43
  • I'm using it only in FilterByTypeFragment. This fragment is used to show meals filtered by category and also to show the list of all the favorite meals – Neeraja Gandla Oct 30 '21 at 19:25
  • 1
    `But if I click on the favorites icon when I'm in the RecipeDetailsFragment, it just displays all the meals of that category.` Can you share your OnClickListener for the favorites icon. Where are you exactly navigating? What I doubt is that you are going to a destination which is already in back stack (i.e. not yet destroyed), that's why you are getting the same instance of viewModel which displays the old data. Will need to see more code to help you out. – Arpit Shukla Oct 31 '21 at 02:50
  • You are right. popping the fragment from the backstack before navigating to the favorites resolved the issue. – Neeraja Gandla Nov 01 '21 at 10:23

1 Answers1

1

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.
Arpit Shukla
  • 9,612
  • 1
  • 14
  • 40
  • Can you point me to any resources that would guide me through the other approaches mentioned here? – Neeraja Gandla Nov 03 '21 at 14:47
  • 1
    To understand the approaches, the best way is to look at the official code samples and documentation and learn from there what are the different ways to structure our code. There are also some good YouTube channels you can search for. – Arpit Shukla Nov 03 '21 at 14:57
  • And you can always post your questions on stack overflow in case of doubts. We would love to help you out. – Arpit Shukla Nov 03 '21 at 14:58