How should i use ViewModel in two fragments? - android

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")
}
}

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.
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)
}
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

Related

How to prevent data duplication caused by LiveData observation in Fragment?

I'm subscribed to an observable in my Fragment, the observable listens for some user input from three different sources.
The main issue is that once I navigate to another Fragment and return to the one with the subscription, the data is duplicated as the observable is handled twice.
What is the correct way to handle a situation like this?
I've migrated my application to a Single-Activity and before it, the subscription was made in the activity without any problem.
Here is my Fragment code:
#AndroidEntryPoint
class ProductsFragment : Fragment() {
#Inject
lateinit var sharedPreferences: SharedPreferences
private var _binding: FragmentProductsBinding? = null
private val binding get() = _binding!!
private val viewModel: ProductsViewModel by viewModels()
private val scanner: CodeReaderViewModel by activityViewModels()
private fun observeBarcode() {
scanner.barcode.observe(viewLifecycleOwner) { barcode ->
if (barcode.isNotEmpty()) {
if (binding.searchView.isIconified) {
addProduct(barcode) // here if the fragment is resumed from a backstack the data is duplicated.
}
if (!binding.searchView.isIconified) {
binding.searchView.setQuery(barcode, true)
}
}
}
}
private fun addProduct(barcode: String) {
if (barcode.isEmpty()) {
return
}
viewModel.insert(barcode)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.start(args.documentId)
if (args.documentType == "Etichette") {
binding.cvLabels.visibility = View.VISIBLE
}
initUI()
observe()
}
private fun observe() {
observeBarcode()
observeProducts()
observeLoading()
observeLast()
}
}
Unfortunately, LiveData is a terribly bad idea (the way it was designed), Google insisted till they kinda phased it out (but not really since it's still there) that "it's just a value holder"...
Anyway... not to rant too much, the solution you have to use can be:
Use The "SingleLiveEvent" (method is officially "deprecated now" but... you can read more about it here).
Follow the "official guidelines" and use a Flow instead, as described in the official guideline for handling UI Events.
Update: Using StateFlow
The way to collect the flow is, for e.g. in a Fragment:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { // or RESUMED
viewModel.yourFlow.collectLatest { ... } // or collect { ... }
}
}
For that in your ViewModel you'd expose something like:
Warning: Pseudo-Code
// Imagine your state is represented in this sealed class
sealed class State {
object Idle: State
object Loading: State
data class Success(val name: String): State
data class Failure(val reason: String): State
}
// You need an initial state
private val _yourFlow = MutableStateFlow(State.Idle)
val yourFlow: StateFlow<State> = _yourFlow
Then you can emit using
_yourFlow.emit(State.Loading)
Every time you call
scanner.barcode.observe(viewLifecycleOwner){
}
You are creating a new anonymous observer. So every new call to observe will add another observer that will get onChanged callbacks. You could move this observer out to be a property. With this solution observe won't register new observers.
Try
class property
val observer = Observer<String> { onChanged() }
inside your method
scanner.barcode.observe(viewLifecycleOwner, observer)
Alternatively you could keep your observe code as is but move it to a Fragment's callback that only gets called once fex. onCreate(). onCreate gets called only once per fragment instance whereas onViewCreated gets called every time the fragment's view is created.

Fragment Never Receives LiveData updates from Shared ViewModel

My app uses MVVM architecture. I have a ViewModel shared by both an Activity and one of its child fragments. The ViewModel contains a simple string that I want to update from the Activity and observe in the fragment.
My issue is simple: the observe callback is never reached in my fragment after the LiveData updates. For testing, I tried observing the data in MainActivity, but that works fine. Additionally, observing LiveData variables in my fragment declared in other ViewModels works fine too. Only this ViewModel's LiveData seems to pose a problem for my fragment, strangely.
I'm declaring the ViewModel and injecting it into my Activity and Fragment via Koin. What am I doing incorrectly to never get updates in my fragment for this ViewModel's data?
ViewModel
class RFIDTagViewModel: ViewModel() {
private val _rfidTagUUID = MutableLiveData<String>()
val rfidTagUUID: LiveData<String> = _rfidTagUUID
fun tagUUIDScanned(tagUUID: String) {
_rfidTagUUID.postValue(tagUUID)
}
}
Activity
class MainActivity : AppCompatActivity(), Readers.RFIDReaderEventHandler,
RFIDSledEventHandler.TagScanInterface {
private val rfidViewModel: RFIDTagViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
rfidViewModel.rfidTagUUID.observe(this, {
Timber.d("I'm ALWAYS reached")
})
}
override fun onResume() {
rfidViewModel.tagUUIDScanned(uuid) //TODO: data passed in here, never makes it to Fragment observer, only observed by Activity successfully
}
}
Fragment
class PickingItemFragment : Fragment() {
private val rfidViewModel: RFIDTagViewModel by viewModel()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
rfidViewModel.rfidTagUUID.observe(viewLifecycleOwner, { tagUUID ->
Timber.d("I'm NEVER reached")
})
}}
Koin DI Config
val appModule = module {
viewModel { RFIDTagViewModel() }
}
In your Fragment I see you are using viewModels(). viewModels() here will be attached to the Fragment, not to the Activity.
If you want to shareViewModel between Fragment and Activity, then in Fragment you use activityViewModels(). Now, in the Fragment, your shareViewModel will be attached to the Activity containing your Fragment.
Edit as follows:
PickingItemFragment.kt
class PickingItemFragment : Fragment() {
private val rfidViewModel: RFIDTagViewModel by activityViewModels()
}
More information: Communicating with fragments
You need to use the same viewmodel, aka, sharedViewModel, the way you are doing you are using two different instances of the same viewmodel.
To fix it.
On both activity and fragment:
private val rfidViewModel: RFIDTagViewModel by activityViewModels()
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=pt-br

How to use viewmodel + room with fragments?

I'm looking through the tutorial for Android room with a view, and trying to extend their model for using ViewModels to multiple fragments, but not really sure how.
MyApplication
class myApplication : Application() {
companion object {
var database: myDatabase? = null
var repository: myRepository? = null
}
override fun onCreate() {
super.onCreate()
database = MyDatabase.getInstance(this)
repository = MyRepository(database!!.myDatabaseDao)
}
}
MyViewModel
class MyViewModel(private val repository: MyRepository) : ViewModel() {
val allWords: LiveData<List<Words>> = repository.allWords.asLiveData()
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
class MyViewModelFactory(private val repository: MyRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return MyViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
HomeFragment
class HomeFragment : Fragment() {
private val myViewModel: MyViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
var rootView = inflater.inflate(R.layout.fragment_home, container, false)
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myViewModel.allWords.observe(viewLifecycleOwner) { words ->
// Update the cached copy of the words in the adapter.
words.let { Log.d("fragment", it.toString()) }
}
}
}
I have a couple of other fragments that will hopefully share the same ViewModel as HomeFragment. I've tried many different approaches, such as using
myViewModel = ViewModelProviders.of(activity!!).get(MyViewModel::class.java)
but all of them give me Caused by: java.lang.InstantiationException: java.lang.Class<com.example.tabtester.ViewModels.MyViewModel> has no zero argument constructor. I can't find any SO posts or documentation that shows me how to provide a constructor in Kotlin.
Also conceptually I can't find any description for what exactly is happening and how the viewmodel is being constructed (and by what). In the Room with a View tutorial, the example given is in MainActivity:
private val wordViewModel: WordViewModel by viewModels {
WordViewModelFactory((application as WordsApplication).repository)
}
This makes sense, to me; you're using the Factory to instantiate a ViewModel to use in the MainActivity. But for any description of how to use ViewModels in Fragments, I don't see where the ViewModel is being constructed. If you have multiple fragments who is constructing the ViewModel? If I use Fragments then does that mean I also need an activity to construct the ViewModel, then somehow share between the Fragments?
Would appreciate any help, or documentation that explains this more clearly.
The underlying APIs of by viewModels(), by activityViewModels() and the (now deprecated) ViewModelProviders.of() all feed into one method: the ViewModelProvider constructor:
ViewModelProvider(viewModelStore: ViewModelStore, factory: ViewModelProvider.Factory)
This constructor takes two parameters:
The ViewModelStore controls the storage and scoping of the ViewModel you create. For example, when you use by viewModels() in a Fragment, it is the Fragment which is used as the ViewModelStore. Similarly, by activityViewModels() uses the Activity as the ViewModelStore.
The ViewModelProvider.Factory controls the construction of the ViewModel if one has not already been created for that particular ViewModelStore.
Therefore if you need a custom Factory, you must always pass that Factory into all places that could create that ViewModel (remember, due to process death and recreation, there's no guarantee that your HomeFragment will be the first fragment to create your ViewModel).
private val myViewModel: MyViewModel by activityViewModels() {
MyViewModelFactory(MyApplication.repository!!)
}
As long as you're using activityViewModels(), the storage of your ViewModel will always be at the activity level, no matter what Factory you are using.

How to observe data from live data android on Three different Fragment

I have my Activity MainActivity.kt .
And and one ViewModel MainActivityViewModel.kt
And I want to observe my live data to my 3 different fragments.
class MainActivity{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
}
class MainFragmentOne{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
}
But my observer only work on activity not on the fragments.
Hey there you are doing everything greate except one thing you should use requireActivity() instead on this in your fragment class.
Make sure your all fragment are attached to your viewModel holding Activity.
class MainActivity{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
}
class MainFragmentOne{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(MainActivityViewModel::class.java)
}
This will help you solve your issue.
For further detail view this.
The ViewModelProviders.of has 2 different constructors:
of(Fragment fragment, ViewModelProvider.Factory factory)
Creates a ViewModelProvider, which retains ViewModels while a scope of
given fragment is alive.
of(FragmentActivity activity, ViewModelProvider.Factory factory)
Creates a ViewModelProvider, which retains ViewModels while a scope of
given Activity is alive.
Basically when you used this as the first parameter in your activity, you passed the context of the activity and created a viewmodel that will be alive in the scope of the activity, however your second this is the context to your fragment, meaning that the second ViewModel will be alive as long as your fragment is alive (only one fragment).
What instead you should be doing in your fragment is using the context of the activity, since activity is always alive when fragments are attached and swapped. You should change your fragments to:
class MainFragmentOne{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(activity!!, viewModelFactory).get(MainActivityViewModel::class.java)
}
or you can use the requireActivity() method that was the previous answer.
To achieve what you are trying to do, you need three things. An activity/fragment that will post the value to the ViewModel, a ViewModel, and an activity/fragment that will retrieve the data from the ViewModel. Lets say your data is stored in an ArrayList, and you want to update and retrieve it from different fragments.
First, we have to implement a ViewModel. It contains the data you want to share between your activities/fragments. You declare the MutableLiveData as an ArrayList then initialize it.
class testviewmodel : ViewModel() {
val list: MutableLiveData<ArrayList<String>> = MutableLiveData()
init {
list.value = arrayListOf()
}
}
Our next step is to access and update the ArrayList using your activity:
val viewmodel = ViewModelProviders.of(this).get(testviewmodel::class.java)
// update the array in Viewmodel
viewmodel.list.postValue(yourarray)
If you are using a Fragment to update it, use this:
activity?.let {
val viewmodel = ViewModelProviders.of(it).get(testviewmodel::class.java)
// update the array in Viewmodel
viewmodel.list.postValue(yourarray)
}
Finally, to retrieve the data from the ViewModel in a fragment, put this inside your onViewCreated:
activity?.let {
val viewmodel = ViewModelProviders.of(it).get(Dbviewmodel::class.java)
observeInput(viewmodel)
}
Put this outside of your onViewCreated:
private fun observeInput(viewmodel: testviewmodel ) {
viewmodel.list.observe(viewLifecycleOwner, Observer {
it?.let {
if (it.size > 5) {
pos = it[5]
//grab it
Toast.makeText(context,pos,Toast.LENGTH_LONG).show()
//display grabbed data
}
}
})
}
Take a look at this docs for more information about ViewModels
Good Luck! I hope this helps :)
That's because you are using the fragment 'this' instance, and not the activity.
Replace
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
With
activity?.let { mainActivityViewModel = ViewModelProviders.of(it, viewModelFactory).get(MainActivityViewModel::class.java) }

Interaction ViewModel and SharedViewModel according to the CleanArchitecture principles

In my app I have some data that will be used across all app in the different fragments. According to the Official Android Guides we should use LiveData and SharedViewModel
That documentations shows just how to use data from SharedViewModel in fragment. But ...
How to use that data in the FragmentViewModel?
Use case #1: using the SharedInfo from SharedViewModel I need to make some request to the server and to do smth with response from server in the FragmentViewModel
Use case #2: I have some screen (fragment) that shows info both from FragmentVM and SharedVM
Use case #3: When user click on SomeButton I need to pass some data from SharedViewModel to the ViewModel
I have found two possibles ways how to do it (maybe their are very similar), but I seems that it can be done more clearly
1) Subscribe to LiveData from SharedViewModel in the fragment and call some method in the ViewModel
2) Use the "CombineLatest" approach like in the RX ( thanks for https://github.com/adibfara/Lives )
Some example to reproduce:
class SharedViewModel(app: Application) : ViewModel(app) {
val sharedInfo = MutableLiveData<InfoModel>()
}
class MyFragmentViewModel(app: Application) : ViewModel(app) {
val otherInfo = MutableLiveData<OtherModel>()
}
class StartFragment : Fragment(){
lateinit var viewModel: MyFragmentViewModel
lateinit var sharedViewModel: SharedViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Create Shared ViewModel in the Activity Scope
activity?.let {
sharedViewModel = ViewModelProviders.of(it).get(SharedViewModel::class.java)
}
// Create simple ViewModel forFragment
viewModel = ViewModelProviders.of(this).get(MyFragmentViewModel::class.java)
// Way #1
sharedViewModel.sharedInfo.observe(this, Observer{
viewModel.toDoSmth(it)
})
viewModel.otherInfo.observe(this, Observer{
sharedViewModel.toDoSmth(it)
})
// Way #2
combineLatest(sharedViewModel.sharedInfo, viewModel.otherInfo){s,o -> Pair(s,o)}.observe(this, Observe{
viewModel.doSmth(it)
// or for example
sharedViewModel.refreshInfo(it)
})
}
}
I expect to found some clear way to access to LiveData from SharedVM from FragmentVm and vise versa. Or maybe I think wrong and this is a bad approach to do that and I shouldn't do it

Categories

Resources