-1

I have an app with one activity and two fragments, in the first fragment, I should be able to insert data to the database, in the second I should be able to see the added items in a recyclerView.

So I've made the Database, my RecyclerView Adapter, and the ViewModel,

the issue is now how should I manage all that?

Should I initialize the ViewModel in the activity and call it in some way from the fragment to use the insert?

Should I initialize the viewmodel twice in both fragments?

My code looks like this:

Let's assume i initialize the viewholder in my Activity:

class MainActivity : AppCompatActivity() {
     private val articoliViewModel: ArticoliViewModel by viewModels {
        ArticoliViewModelFactory((application as ArticoliApplication).repository)
    }
}

Then my FirstFragments method where i should add the data to database using the viewModel looks like this:

class FirstFragment : Fragment() {
    private val articoliViewModel: ArticoliViewModel by activityViewModels()
    private fun addArticolo(barcode: String, qta: Int) { // function which add should add items on click
      // here i should be able to do something like this

        articoliViewModel.insert(Articolo(barcode, qta))
    }
}

And my SecondFragment

class SecondFragment : Fragment() {    
    private lateinit var recyclerView: RecyclerView
    private val articoliViewModel: ArticoliViewModel by activityViewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        recyclerView = view.findViewById(R.id.recyclerView)
        val adapter = ArticoliListAdapter()
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(activity)
        // HERE I SHOULD BE ABLE DO THIS   
        articoliViewModel.allWords.observe(viewLifecycleOwner) { articolo->
            articolo.let { adapter.submitList(it) }
        }

    } 
}

EDIT:

My ViewModel looks like this:

class ArticoliViewModel(private val repository: ArticoliRepository): ViewModel() {
    val articoli: LiveData<List<Articolo>> = repository.articoli.asLiveData()

    fun insert(articolo: Articolo) = viewModelScope.launch {
        repository.insert(articolo)
    }
}

class ArticoliViewModelFactory(private val repository: ArticoliRepository): ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(ArticoliViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return ArticoliViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }

}
NiceToMytyuk
  • 3,644
  • 3
  • 39
  • 100
  • If you initialize the viewmodel twice, you will have two separate instances and I think this defeats your stated purpose. – Kristy Welsh Jan 19 '21 at 14:40
  • @KristyWelsh so at this point i should do it in the Activity as i was doing in the example, but how could i use it in the Fragment once it's initialized in the Activity? – NiceToMytyuk Jan 19 '21 at 14:43
  • What's stopping you from having a different type of ViewModel for each Fragment? They can both observe a data source from the same Repository – Ivan Wooll Jan 19 '21 at 14:58
  • @IvanWooll actually i was asking if it was a good practice of initializing two ViewModels in fragments – NiceToMytyuk Jan 19 '21 at 14:59
  • Use `by ActivityViewModels()` to initialize it in both Fragments. They will receive the same instance. If there is overlap of data that both Fragments show, you may want them to have the same ViewModel so transitions between the Fragments will be faster. – Tenfour04 Jan 19 '21 at 15:01
  • @Tenfour04 how should i initialize it in the Activity if i would use by ActivityViewModels? as by trying to let it as in my question my application returns "Cannot create an instance of class ArticolIViewModel" – NiceToMytyuk Jan 19 '21 at 15:06
  • What is the complete stack trace? – Tenfour04 Jan 19 '21 at 15:09
  • @Tenfour04 i published in [here](https://justpaste.it/7vcof) – NiceToMytyuk Jan 19 '21 at 15:21
  • @Tenfour04 that happen when i run viewModel.insert in my Fragment – NiceToMytyuk Jan 19 '21 at 15:22
  • The stack trace is showing that you are using the default view model factory instead of your own. But that doesn't make sense, because in your code above you are explicitly using your own. – Tenfour04 Jan 19 '21 at 15:29
  • @Tenfour04 i just updated my code of how the Activity and my fragments looks like now.. – NiceToMytyuk Jan 19 '21 at 15:33
  • 1
    OK, so if your Fragment tries to instantiate it first, it's not using the custom factory, so it fails. I'll write a more detailed answer. – Tenfour04 Jan 19 '21 at 15:36

1 Answers1

5

Whether multiple fragments should share a ViewModel depends on whether they are showing the same data. If they show the same data, I think it usually makes sense to share a ViewModel so the data doesn't have to be pulled from the repository when you switch between them, so the transition is faster. If either of them also has significant amount of unique data, you might consider breaking that out into a separate ViewModel so it doesn't take up memory when it doesn't need to.

Assuming you are using a shared ViewModel, you can do it one of at least two different ways, depending on what code style you prefer. There's kind of a minor trade-off between encapsulation and code duplication, although it's not really encapsulated anyway since they are looking at the same instance. So personally, I prefer the second way of doing it.

  1. Each ViewModel directly creates the ViewModel. If you use by activityViewModels(), then the ViewModel will be scoped to the Activity, so they will both receive the same instance. But since your ViewModel requires a custom factory, you have to specify it in both Fragments, so there is a little bit of code duplication:
// In each Fragment:
private val articoliViewModel: ArticoliViewModel by activityViewModels {
    ArticoliViewModelFactory((application as ArticoliApplication).repository)
}
  1. Specify the ViewModel once in the MainActivity and access it in the Fragments by casting the activity.
// In Activity: The same view model code you already showed in your Activity, but not private

// In Fragments:
private val articoliViewModel: ArticoliViewModel
    get() = (activity as MainActivity).articoliViewModel

Or to avoid code duplication, you can create an extension property for your Fragments so they don't have to have this code duplication:

val Fragment.articoliViewModel: ArticoliViewModel
    get() = (activity as MainActivity).articoliViewModel
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Ended up by using the 2nd solution as in the fragment i was unable to use applcation as you mentioned in the 1st one, even the 3rd works as it have to, thank you. Now i have the problem with "cannot access database on the main thread" but ill make a new question for it. – NiceToMytyuk Jan 19 '21 at 15:56
  • 1
    Actually, look here. That question from today is a duplicate and has poor answers. https://stackoverflow.com/questions/44167111/android-room-simple-select-query-cannot-access-database-on-the-main-thread – Tenfour04 Jan 19 '21 at 16:01
  • 1
    Look at the Kotlin Coroutines answer, as the accepted answer is out-of-date. – Tenfour04 Jan 19 '21 at 16:02
  • I actually followed step by step [this official guide](https://developer.android.com/codelabs/android-room-with-a-view-kotlin#17) and in the example the data is insert without problems and it's using coroutines.. – NiceToMytyuk Jan 19 '21 at 16:12