How can I share LiveData between multiple ViewModels? - android

I've tried extracting the value into a base class and having the ViewModels extend it. When I do that, however, the Observer isn't sticking to the LiveData. For instance, when I have a parent class with LiveData:
class Base : ViewModel() {
private val _ data = MutableLiveData()
val data: LiveData = _data
fun someEvent(foo: Foo) { // update _data }
}
class Derived : Base()
class Derived1 : Base()
Then get one of those ViewModels and observe data:
class Frag : Fragment {
onViewCreated() {
// get Derived, ViewModelProviders.of ...etc
derived.data.observe { // Doesn't observe changes }
}
}
Calling Base.someEvent(foo) doesn't notify the LiveData in the Fragment.
I want to avoid getting a reference to both subclasses and invoking someEvent on each. One thing to note is that I'm using a single Activity approach and all ViewModels are Activity scoped.

class Derived : Base()
and
class Derived1 : Base()
have their own instance of:
private val _ data = MutableLiveData()
val data: LiveData = _data
that means you need to
derived.data.observe { // do something }
derived1.data.observer { // do something }
derived.someEvent(someFoo)
derived1.someEvent(someFoo)
You are trying to achieve something in a wrong way.

Related

Type cast / make genric viewModel to be passed as parameter

I am working on a custom dialog fragment, that is being used/ called from two different views having different viewModels. Instead of passing two separate viewModels in the constructor parameter of Dialog class as,
class CustomeDialog(var viewModel1: ViewModelA ?= null, var viewModel2 : ViewModelB ?= null) : DialogFragment()
I need to ask/ figure out a way where I could just set < T> kind of parameter to dialog so I could just type caste any viewModel to it, I want.
something like this,
class CustomDialog<T:ViewModel> : DialogFragment()
and in code, it would be something like
val mdialog1: CustomeDialog by lazy { CustomeDialog(viewModel as ViewModelA) }
and also
val mdialog2: CustomeDialog by lazy { CustomeDialog(viewMode2 as ViewModelB) }
You can create a secondary constructor in the generic class that takes in a generic ViewModel parameter:
class CustomeDialog<T : ViewModel>() : DialogFragment() {
constructor(viewmodel: T) : this()
}
And the usage the same as you did:
lateinit var viewModel: ViewModel
val mdialog1: CustomeDialog<ViewModelA> by lazy { CustomeDialog(viewModel as ViewModelA) }
lateinit var viewModel2: ViewModelA
val mdialog2: CustomeDialog<ViewModelA> by lazy { CustomeDialog(viewModel2) }
UPDATE:
how to initialize viewModel in dialog based on the type. eg. if VM1 is passed in constructor, then var dialogViewModel = WHAT??,
It's requested to have a Dialog with a generic ViewModel, so its type is Generic as it's unknown till it's instantiated.
yeah i need a local var dialogViewModel which is generic, as i mentioned, whole logic is dependent on this dvm
You can initialize it in the secondary constructor:
class CustomDialog<T : ViewModel>() : DialogFragment() {
lateinit var dialogViewModel: T
constructor(viewmodel: T) : this() {
dialogViewModel = viewmodel
}
}
This strategy cannot work. The OS recreates your Fragment using reflection and its empty constructor. It can restore state to the replacement Fragment using Bundle values, but class types are not a valid type of data for a Bundle.
Closest I can come up with is to make it an abstract class, and then create simple subclasses that have concrete types.
abstract class CustomDialog<T: ViewModel>(viewModelType: KClass<out T>): DialogFragment() {
val viewModel: T by createViewModelLazy(viewModelType, { viewModelStore })
}
class CustomDialogA: CustomDialog<ViewModelA>(ViewModelA::class)
class CustomDialogB: CustomDialog<ViewModelB>(ViewModelB::class)

How should i use ViewModel in two fragments?

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

Multiple instances of same fragment type with different ViewModel type based on argument passed to fragment

I have application with ViewPager2. All pages in it are instances of same type fragmet. All pages are displaying some lists in a recyclerview, of different data types. Data are populated from some API endpoint, using Retrofit calls.
I'm using dagger 2 to inject viewmodels.
Everything is working fine with one page. Now I want to create other pages, and I'm asking is there any method/pattern to implements other pages without creating different fragment classes, but only to use different viewModel type?
Maybe some generic viewModel or something.
PageType
enum class PageType {
Page1, Page2, Page3;
}
val PageType.viewModelClass: KClass<out GenericViewModel>
get() = when (this) {
PageType.Page1 -> Page1ViewModel::class
// todo
}
ViewModel
abstract class GenericViewModel<T>() : BaseViewModel() {
abstract val pageType: PageType
}
class Page1ViewModel #Inject constructor() : GenericViewModel() {
override val pageType: PageType = PageType.Page1
}
Fragment
class PageFragment : BaseFragment() {
private val pageType: PageType by lazy {
TODO("read from arguments bundle")
}
private val vm: GenericViewModel by lazy {
ViewModelProvider(this, viewModelFactory).get(pageType.viewModelClass.java)
}
companion object {
fun newInstance(type: PageType): PageFragment {
TODO("add type into bundle and set as arguments")
}
}
}
If your current dagger setup works for viewmodels, then you won't have to do anything special to support this pattern.

How to use Dagger 2 to Inject ViewModel of same Fragments inside ViewPager

I am trying to add Dagger 2 to my project. I was able to inject ViewModels (AndroidX Architecture component) for my fragments.
I have a ViewPager which has 2 instances of the same fragment (Only a minor change for each tabs) and in each tab, I am observing a LiveData to get updated on data change (from API).
The issue is that when the api response comes and updates the LiveData, the same data in the currently visible fragment is being sent to observers in all the tabs. (I think this is probably because of the scope of the ViewModel).
This is how I am observing my data:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
activityViewModel.expenseList.observe(this, Observer {
swipeToRefreshLayout.isRefreshing = false
viewAdapter.setData(it)
})
....
}
I am using this class for providing ViewModels:
class ViewModelProviderFactory #Inject constructor(creators: MutableMap<Class<out ViewModel?>?, Provider<ViewModel?>?>?) :
ViewModelProvider.Factory {
private val creators: MutableMap<Class<out ViewModel?>?, Provider<ViewModel?>?>? = creators
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
var creator: Provider<out ViewModel?>? = creators!![modelClass]
if (creator == null) { // if the viewmodel has not been created
// loop through the allowable keys (aka allowed classes with the #ViewModelKey)
for (entry in creators.entries) { // if it's allowed, set the Provider<ViewModel>
if (modelClass.isAssignableFrom(entry.key!!)) {
creator = entry.value
break
}
}
}
// if this is not one of the allowed keys, throw exception
requireNotNull(creator) { "unknown model class $modelClass" }
// return the Provider
return try {
creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
companion object {
private val TAG: String? = "ViewModelProviderFactor"
}
}
I am binding my ViewModel like this:
#Module
abstract class ActivityViewModelModule {
#MainScope
#Binds
#IntoMap
#ViewModelKey(ActivityViewModel::class)
abstract fun bindActivityViewModel(viewModel: ActivityViewModel): ViewModel
}
I am using #ContributesAndroidInjector for my fragment like this:
#Module
abstract class MainFragmentBuildersModule {
#ContributesAndroidInjector
abstract fun contributeActivityFragment(): ActivityFragment
}
And I am adding these modules to my MainActivity subcomponent like this:
#Module
abstract class ActivityBuilderModule {
...
#ContributesAndroidInjector(
modules = [MainViewModelModule::class, ActivityViewModelModule::class,
AuthModule::class, MainFragmentBuildersModule::class]
)
abstract fun contributeMainActivity(): MainActivity
}
Here is my AppComponent:
#Singleton
#Component(
modules =
[AndroidSupportInjectionModule::class,
ActivityBuilderModule::class,
ViewModelFactoryModule::class,
AppModule::class]
)
interface AppComponent : AndroidInjector<SpenmoApplication> {
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: Application): Builder
fun build(): AppComponent
}
}
I am extending DaggerFragment and injecting ViewModelProviderFactory like this:
#Inject
lateinit var viewModelFactory: ViewModelProviderFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
....
activityViewModel =
ViewModelProviders.of(this, viewModelFactory).get(key, ActivityViewModel::class.java)
activityViewModel.restartFetch(hasReceipt)
}
the key will be different for both the fragments.
How can I make sure that only the observer of the current fragment is getting updated.
EDIT 1 ->
I have added a sample project with the error. Seems like the issue is happening only when a custom scope is added. Please check out the sample project here: Github link
master branch has the app with the issue. If you refresh any tab (swipe to refresh) the updated value is getting reflected in both the tabs. This is only happening when I add a custom scope to it (#MainScope).
working_fine branch has the same app with no custom scope and its working fine.
Please let me know if the question is not clear.
I want to recap the original question, here's it:
I am currently using the working fine_branch, but I want to know, why would using scope break this.
As per my understanding your have an impression, that just because you are trying to obtain an instance of ViewModel using different keys, then you should be provided different instances of ViewModel:
// in first fragment
ViewModelProvider(...).get("true", PagerItemViewModel::class.java)
// in second fragment
ViewModelProvider(...).get("false", PagerItemViewModel::class.java)
The reality, is a bit different. If you put following log in fragment you'll see that those two fragments are using the exact same instance of PagerItemViewModel:
Log.i("vvv", "${if (oneOrTwo) "one:" else "two:"} viewModel hash is ${viewModel.hashCode()}")
Let's dive in and understand why this happens.
Internally ViewModelProvider#get() will try to obtain an instance of PagerItemViewModel from a ViewModelStore which is basically a map of String to ViewModel.
When FirstFragment asks for an instance of PagerItemViewModel the map is empty, hence mFactory.create(modelClass) is executed, which ends up in ViewModelProviderFactory. creator.get() ends up calling DoubleCheck with following code:
public T get() {
Object result = instance;
if (result == UNINITIALIZED) { // 1
synchronized (this) {
result = instance;
if (result == UNINITIALIZED) {
result = provider.get();
instance = reentrantCheck(instance, result); // 2
/* Null out the reference to the provider. We are never going to need it again, so we
* can make it eligible for GC. */
provider = null;
}
}
}
return (T) result;
}
The instance is now null, hence a new instance of PagerItemViewModel is created and is saved in instance (see // 2).
Now the exact same procedure happens for SecondFragment:
fragment asks for an instance of PagerItemViewModel
map now is not empty, but does not contain an instance of PagerItemViewModel with key false
a new instance of PagerItemViewModel is initiated to be created via mFactory.create(modelClass)
Inside ViewModelProviderFactory execution reaches creator.get() whose implementation is DoubleCheck
Now, the key moment. This DoubleCheck is the same instance of DoubleCheck that was used for creating ViewModel instance when FirstFragment asked for it. Why is it the same instance? Because you've applied a scope to the provider method.
The if (result == UNINITIALIZED) (// 1) is evaluating to false and the exact same instance of ViewModel is being returned to the caller - SecondFragment.
Now, both fragments are using the same instance of ViewModel hence it is perfectly fine that they are displaying the same data.
Both the fragments receive the update from livedata because viewpager keeps both the fragments in resumed state.
Since you require the update only on the current fragment visible in the viewpager, the context of the current fragment is defined by the host activity, the activity should explicitly direct updates to the desired fragment.
You need to maintain a map of Fragment to LiveData containing entries for all the fragments(make sure to have an identifier that can differentiate two fragment instances of the same fragment) added to viewpager.
Now the activity will have a MediatorLiveData observing the original livedata observed by the fragments directly. Whenever the original livedata posts an update, it will be delivered to mediatorLivedata and the mediatorlivedata in turen will only post the value to livedata of the current selected fragment. This livedata will be retrieved from the map above.
Code impl would look like -
class Activity {
val mapOfFragmentToLiveData<FragmentId, MutableLiveData> = mutableMapOf<>()
val mediatorLiveData : MediatorLiveData<OriginalData> = object : MediatorLiveData() {
override fun onChanged(newData : OriginalData) {
// here get the livedata observed by the currently selected fragment
val currentSelectedFragmentLiveData = mapOfFragmentToLiveData.get(viewpager.getSelectedItem())
// now post the update on this livedata
currentSelectedFragmentLiveData.value = newData
}
}
fun getOriginalLiveData(fragment : YourFragment) : LiveData<OriginalData> {
return mapOfFragmentToLiveData.get(fragment) ?: MutableLiveData<OriginalData>().run {
mapOfFragmentToLiveData.put(fragment, this)
}
}
class YourFragment {
override fun onActivityCreated(bundle : Bundle){
//get activity and request a livedata
getActivity().getOriginalLiveData(this).observe(this, Observer { _newData ->
// observe here
})
}
}

LiveData is abstract android

I tried initializing my LiveData object and it gives the error: "LiveData is abstract, It cannot be instantiated"
LiveData listLiveData = new LiveData<>();
In a ViewModel, you may want to use MutableLiveData instead.
E.g.:
class MyViewModel extends ViewModel {
private MutableLiveData<String> data = new MutableLiveData<>();
public LiveData<String> getData() {
return data;
}
public void loadData() {
// Do some stuff to load the data... then
data.setValue("new data"); // Or use data.postValue()
}
}
Or, in Kotlin:
class MyViewModel : ViewModel() {
private val _data = MutableLiveData<String>()
val data: LiveData<String> = _data
fun loadData() {
viewModelScope.launch {
val result = // ... execute some background tasks
_data.value = result
}
}
}
Since it is abstract (as #CommonsWare says) you need to extend it to a subclass and then override the methods as required in the form:
public class LiveDataSubClass extends LiveData<Location> {
}
See docs for more details
Yes, you cannot instantiate it because it is an abstract class. You can try to use MutableLiveData if you want to set values in the live data object. You can also use Mediator live data if you want to observe other livedata objects.
You need to use MutableLiveData and then cast it to its parent class LiveData.
public class MutableLiveData
extends LiveData
[MutableLiveData is] LiveData which publicly exposes setValue(T) and postValue(T) method.
You could do something like this:
fun initializeLiveData(foo: String): LiveData<String> {
return MutableLiveData<String>(foo)
}
So then you get:
Log.d("now it is LiveData", initializeLiveData("bar").value.toString())
// prints "D/now it is LiveData: bar"
I think much better way of achieving this is by using, what we call is a Backing Property, to achieve better Encapsulation of properties.
Example of usage:
private val _userPoints = MutableLiveData<Int>()// used as a private member
val userPoints: LiveData<Int>
get() {
return _userPoints
} //used to access the value inside the UI controller (Fragment/Activity).
Doing so maintains an editable MutableLiveData private to ViewModel class while, the read-only version of it is maintained as a LiveData, with a getter that returns the original value.
P.S. - notice the naming convention for both fields, using an (_) underscore. This is not mandatory but advised.

Categories

Resources