Android Room ViewModel initialization in fragment crashes app

Viewed 272

The situation is pretty straightforward. I have a simple android app with 4 fragments displayed through a bottom navigation bar, and a central Room database. Each fragment should be able to perform CRUD operations on the DB through a viewmodel (details are probably irrelevant but I'll show this as well to be sure):

class ViewModel(application: Application): AndroidViewModel(application) {
    val readAllIngredients: LiveData<List<Ingredient>>
    val readAllRecipes: LiveData<List<Recipe>>
    private val ingredientRepository: IngredientRepository
    private val recipeRepository: RecipeRepository

    init {
        val ingredientDAO = ShoppingAppDatabase.getDatabase(application).ingredientDAO()
        val recipeDAO = ShoppingAppDatabase.getDatabase(application).recipeDAO()
        ingredientRepository = IngredientRepository(ingredientDAO)
        recipeRepository = RecipeRepository(recipeDAO)
        readAllIngredients = ingredientRepository.allIngredients
        readAllRecipes = recipeRepository.allRecipes
    }

    fun addIngredient(ingredient: Ingredient) {
        viewModelScope.launch(Dispatchers.IO) {
            ingredientRepository.put(ingredient)
        }
    }

    fun deleteIngredient(ingredient: Ingredient) {
        viewModelScope.launch(Dispatchers.IO) {
            ingredientRepository.delete(ingredient)
        }
    }

    fun addRecipe(recipe: Recipe) {
        viewModelScope.launch(Dispatchers.IO) {
            recipeRepository.put(recipe)
        }
    }

    fun updateRecipe(recipe: Recipe) {
        viewModelScope.launch(Dispatchers.IO) {
            recipeRepository.update(recipe)
        }
    }

    fun updateIngredient(ingredient: Ingredient) {
        viewModelScope.launch(Dispatchers.IO) {
            ingredientRepository.update(ingredient)
        }
    }
}

I'm initializing a viewmodel in each fragment, but with limited success. Here's a fragment for which everything works fine:

class InventoryFragment() : Fragment() {
    private var listAdapter = IngredientAdapter()
    private lateinit var viewModel : ViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

    }

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


        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_inventory, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        listAdapter = IngredientAdapter()
        recycler_view.apply {
            layoutManager = LinearLayoutManager(activity)
            adapter = listAdapter
        }
        viewModel = ViewModelProvider(this).get(ViewModel::class.java)
        viewModel.readAllIngredients.observe(viewLifecycleOwner, Observer { ingredient -> listAdapter.setData(ingredient) })
        add_button.setOnClickListener{
            val errorMessages = validateInput()
            if(errorMessages.isNotEmpty()) {
                displayToast(activity, errorMessages)
            }
            else {
                viewModel.addIngredient(Ingredient(
                    edit_name.text.toString(),
                    edit_qty.text.toString().toFloat(),
                    edit_um.text.toString()
                ))
                listAdapter.notifyDataSetChanged()
                displayToast(activity, "Ingredient added")
                hideKeyboard(activity, requireView().windowToken)
            }
            clearInput()
        }
    }
}

I'm initializing it in the onViewCreated callback and yeah, it works fine. Doing the same thing in a different fragment yields.. different results for some reason.

class BrowseFragment() : Fragment() {
    private lateinit var viewModel: ViewModel
    var recipeAdapter = RecipeAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_browse, container, false)
    }


    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel = ViewModelProvider(this).get(ViewModel::class.java)
        viewModel.readAllRecipes.observe(viewLifecycleOwner, Observer { recipe -> recipeAdapter.setData(recipe) })
        submit_button.setOnClickListener{
            var submitFragment = SubmitFragment(recipeAdapter)
            var tr = (view.context as FragmentActivity).supportFragmentManager.beginTransaction()
            tr.replace(R.id.fragment_container, submitFragment)
            tr.commit()
        }
        browse_recycler_view.apply {
            layoutManager = LinearLayoutManager(activity)
            adapter = recipeAdapter
        }
    }
}

When I try to initialize the viewmodel in onViewCreated, I get an IllegalStateException: Can't access ViewModels from detached fragment exception. Creating it in onCreate doesn't work either, since the lifecycle owner is null, which makes sense I guess. What exactly am I doing wrong here?

0 Answers
Related