Use ViewModelFactory inside Fragment - android

I'm trying to share a ViewModel between my activity and my fragment. My ViewModel contains a report, which is a complex object I cannot serialize.
protected val viewModel: ReportViewModel by lazy {
val report = ...
ViewModelProviders.of(this, ReportViewModelFactory(report)).get(ReportViewModel::class.java)
}
Now I'm trying to access the viewmodel in a fragment, but I don't want to pass all the factory parameters again.
As stated by the ViewModelProvider.get documentation:
Returns an existing ViewModel or creates a new one in the scope
I want to access the ViewModel instance defined in the activity, so I tried the following but it logically crashes as the model doesn't have an empty constructor:
protected val viewModel: ReportViewModel by lazy {
ViewModelProviders.of(requireActivity()).get(ReportViewModel::class.java)
}
How one should access its "factorysed" ViewModels in a fragment? Should we pass the factory to the fragment?
Thanks!

A little late but I had this question myself. What I found is you can do the following:
In your activity override getDefaultViewModelProviderFactory() like so:
override fun getDefaultViewModelProviderFactory(): ReportViewModelFactory {
return ReportViewModelFactory(report)
}
now in your fragments you can do
requireActivity().getDefaultViewModelProviderFactory()
to get the factory.
Or simply instantiate your viewModel like:
private val viewModel: ReportViewModel by activityViewModels()

Related

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

How do I initialize viewmodel in addedditactivity, is there a better alternative?

I have this database that contains information about celebrities, lots of information, ie movies, roles, articles...
Without the viewmodel, all of the stuff in the edit text gets lost. So i dont want to lose all of that
info on rotation.
So i send an intent with all the relevant info of a selected celebrity(sql room) to the addeditactivity, so now how do i initialize the viewmodel? If I do getStringExtra in onCreate, wouldn't that just rewrite the viewmodel again when the activity gets recreated?
how do i get around this, also is there a better alternative? im a beginner, thanks in advance!
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ......... shared preferences code .................
// get intent code ................
mViewModel = ViewModelProviders.of(this).get(testViewModel.class);
mViewModel.celebName = intent.getStringExtra(CELEB_NAME);
// similar..........
}
No, ViewModels have separate lifecyles from Activities. When your Activity gets rotated/recreated it will still use your previously-instantiated ViewModel (and any data saved in it) unless it has already been cleared.
See https://developer.android.com/topic/libraries/architecture/viewmodel
If the activity is re-created, it receives the same MyViewModel
instance that was created by the first activity. When the owner
activity is finished, the framework calls the ViewModel objects's
onCleared() method so that it can clean up resources.
You can initialize your viewModel in onCreate() using ViewModelFactory to pass your data from getStringExtra to your viewModel. Use viewModelFactory pattern to pass data to your viewModel. Here "YourINFOParameter" is your data from getStringExtra. So after initialization in your viewModel you have set parameter you can observe using liveData. I am suing Kotlin in answer.
as example:
override fun onCreateView(
........
val viewModelFactory =
TestViewModelFactory(
yourINFOparameter,
application)
val testViewModel = ViewModelProvider(
this, viewModelFactory
).get(TestViewModel::class.java)
.................
Then declare class TestViewModelFactory:
class TestViewModelFactory(
private val yourINFOparameter: String, private val application: Application) : ViewModelProvider.Factory {
#Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(TestViewModel::class.java)) {
return TestViewModel(yourINFOparameter,
application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
And your viewModelclass:
class TestViewModel(yourINFOparameter: String, application: Application) : AndroidViewModel(application) {
............................}

Android ViewModelProvider() parameter error

I am trying to get a value from the SharedViewModel class but the ViewModelProvider() is giving a parameter error when i am passing requireActivity() although the same initilization and assignment works in my fragments.
It is requiring "ViewModelStoreOwner" to be passed.
class CourseRepository(val app: Application) {
private var viewModel: SharedViewModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)
val courseData = MutableLiveData<List<Course>>()
init {
CoroutineScope(Dispatchers.IO).launch {
callWebService()
}
}
#WorkerThread
suspend fun callWebService() {
if (Utility.networkAvailable(app)) {
val retrofit = Retrofit.Builder().baseUrl(WEB_SERVICE_URL).addConverterFactory(MoshiConverterFactory.create()).build()
val service = retrofit.create(CourseService::class.java)
val serviceData = service.getCourseData(viewModel.pathName).body() ?: emptyList()
courseData.postValue(serviceData)
}
}
}
The purpose of the ViewModel here is because i am storing the Id of the selected RecyclerView item in order to send it to a server
ViewModel instances are scoped to Fragments or Activities (or anything with a similar lifecycle), which is why you need to pass in a ViewModelStoreOwner to the provider to get a ViewModel from it. The point of ViewModels is that they will exist until the store they belong to is destroyed.
The requireActivity method doesn't work here, because you're not inside a Fragment.
Some things to consider here:
Do you really need ViewModel in this use case? Could you perhaps use just a regular class that you can create by calling its constructor?
Could you call this Repository from your ViewModel, and pass in any parameters you need from there?

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