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) {
............................}
Related
I am trying to understand how can I use my same viewmodel across different fragments which even belongs to different activities.
So let's say I have Activity1 with Fragment A, Fragment B and Activity2 with Fragment C. How do I create a single instance of viewmodel that I can use across all these fragments.
I tried understanding shared viewmodel but seems like it is to be used if sharing data between fragments of a single activity and not multiple activities.
So basically I want to create a single instance of viewmodel across all the fragments? How can I achieve this functionality also keeping in mind the MVVM approach.
Any help would be appreciated. Thanks!
This is not supported. Google's recommendation is to put all your screens in a single Activity.
But, you can make an intermediate singleton class that each instance of the ViewModel uses.
Or maybe you could use a factory that treats it like a temporary singleton and does reference counting so it doesn't get cleared too early or hang onto the reference for too long. Untested example of what I mean:
private var viewModelInstance: MyViewModel? = null
private var refCount = 0
class MyViewModel: ViewModel() {
override fun onCleared() {
if (--refCount > 0) {
return
}
viewModelInstance = null
// Do typical onCleared cleanup here
}
}
class MyViewModelFactory: ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
require(modelClass.isAssignableFrom(MyViewModel::class.java)) { "Factory only supports MyViewModel" }
refCount++
viewModelInstance = viewModelInstance ?: MyViewModel()
return viewModelInstance as T
}
}
Shared viewmodel between multiple activities is not supported.
One way to achieve this using AndroidViewModel. You can create a ViewModel extending AndroidViewModel . This requires application instance. This viewmodel will be binded to application lifecycle and same instance will be available through out the lifecycle of the application. In one activity you can add data, and in other activity you can get updated data.
This will be acting something like singleton instance(But not exactly).
Addition to this, you can also use live data in AndroidViewModel if you use observer with activity/fragment lifecycle owner. So the observer will be live only till life cycle of fragment or activity.
ViewModel:
class AppViewModel constructor(private val mApplication: Application) :
AndroidViewModel(mApplication) {
//ViewModel Logic
}
Initializing Viewmodel:
You can initialize viewmodel like this in any fragment or activity.
val appViewModel = ViewModelProvider.AndroidViewModelFactory.getInstance(MyApp.getInstance())
.create(AppViewModel::class.java)
Application Class:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
mInstance = this
}
companion object {
lateinit var mInstance: CooperApp
#Synchronized
fun getInstance(): MyApp {
return mInstance
}
}
}
Also one more thing we can use is like initializing viewmodel in application class and create similar function to getInstance() which will return viewmodel instance and use it all over the app
I've successfully implemented repository based MVVM. However I need to pass a class object between fragments. I've implemented a sharedViewModel between multiple fragments but the set value always gives null. I know this is due to me not passing the activity context to the initialization of the viewmodels in fragments. I am working with ModelFactory to make instances of my viewmodel yet I can't figure out a way to give 'applicationActivity()' .
Here's my modelFactory:
class MyViewModelFactory constructor(private val repository: MyRepository): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(MyOwnViewModel::class.java)) {
MyOwnViewModel(this.repository) as T
} else {
throw IllegalArgumentException("ViewModel Not Found")
}
}
and this is how I intialize my viewmodel:
viewModel=ViewModelProvider(this, MyViewModelFactory(
MyRepository(MyServices() ) )).get(MyOwnViewModel::class.java)
fetching data and everything else works, but I need to be able to share data between fragments and i can't do that with this architecture. I'm not using dagger or Hilt.
Thank you for any pointers.
You can use by activityViewModels() and pass the factory
private val myViewModel: MyViewModel by activityViewModels(factoryProducer = {
MyViewModelFactory(<your respository instance>)
})
It would be good idea to get your repository instance from a singleton or from a field in Application class. If you choose to get from an Application class you can do it like this;
class MyApp: Application() {
val service by lazy { MyService() }
val repository by lazy { MyRepository(service) }
}
by defining them lazy, it ensures that they are not initialized until its necessary
With your application class, your viewmodel call should look like this
private val myViewModel: MyViewModel by activityViewModels(factoryProducer = {
MyViewModelFactory((activity?.application as MyApp).repository)
})
You can also write viewmodelfactory this way
class MyViewModelFactory(internal var viewModel: ViewModel) : ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
return viewModel as T
}
}
And for share data between fragment you can use bundle
In my Android app, I pass custom data (UByteArray) from one activity to another using the parcelable interface.
I am using this data inside multiple fragments, so I rewrote the data class to extend androidx ViewModel and expose LiveData properties to the fragments. Now the UI updates are a lot nicer, but I think I am using it wrong because I overwrite all ViewModel values inside onCreate.
Now my question: What do I need to change to initialize the ViewModel only once?
The following is my current code (abbreviated and renamed for this question):
class ActivityB : AppCompatActivity() {
private val bData: ViewModelB by viewModels()
// ...
override fun onCreate(savedInstanceState: Bundle?) {
// ...
intent.getParcelableExtra<ViewModelB>("id")?.let {
Log.e(TAG, "Found parceled bData $it")
// This seems like a very stupid way to do it, is there a better one?
bData.copyAll(it)
}
}
}
I saw that it is possible to inject SavedState into the ViewModelB constructor, but I don't have a saved state until now, and the data needs to be passed only once.
Should I change the initialization of tagData with by viewModels() to = ViewModelB(intent)?
Or do I need to extend the ViewModelFactory somehow?
Any tip here would be really appreciated, thanks.
I saw that it is possible to inject SavedState into the ViewModelB constructor, but I don't have a saved state until now, and the data needs to be passed only once.
The official solution would be to provide a SavedStateHandle that is initialized with the defaultArgs as the intent.extras of your Activity.
For that, you need to provide an AbstractSavedStateViewModelFactory implementation, OR use SavedStateViewModelFactory (in which case you must define the right constructor in order to have it instantiated via reflection).
class ActivityB : AppCompatActivity() {
private val bData: ViewModelB by viewModels {
SavedStateViewModelFactory(application, this, intent.extras)
}
// ...
override fun onCreate(savedInstanceState: Bundle?) {
// ...
// intent.getParcelableExtra<ViewModelB>("id")?.let {
// Log.e(TAG, "Found parceled bData $it")
}
}
Then in your ViewModel
#Keep
class ViewModelB(val savedStateHandle: SavedStateHandle): ViewModel() {
val uByteData = savedStateHandle.get<UByteArray>("id")
}
Or so. The "id" key must match the same key as is in the intent extras.
Since you have a ViewModel which implements Parcelable, you can get your ViewModelB instance directly from the Intent extra.
The Intent which is used for starting ActivityB may not be != null at the time when ActivityB is instantiated, but you can use
lateinit var bData: ViewModelB
Then in onCreate()
bData = if(intent.hasExtra("id")) intent.getParcelableExtra<ViewModelB>("id") else ViewModelProvider(this).get(ViewModelB::class.java)
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()
I am using Koin library in Kotlin for DI
Koin providing by viewmodel() for get instance of ViewModel by sharedViewModel() to get same instance in fragments.
How can I get same instance of the ViewModel in activities ? I didn't find any way to achieve this.
you must use single{} instead of viewModel{} in module declaration.
single { SharedViewModel() }
And, you can use viewModel() in your views.
View1
private val viewModel: SharedViewModel by viewModel()
View2
private val viewModel: SharedViewModel by viewModel()
But you must load modules when view start by
loadKoinModules(module1)
The important point is that you must unload module in when destroy view.
unloadKoinModules(mainModule)
So, when unload modules your singleton ViewModel will be destroyed.
#EDIT
Now, you can use sharedViewModel declaration.
After some research or discussion on architecture level and also report and issue github Koin,i found solution for this
In this scenario,We should save that state/data into Repository which we need to share between multiple activities not in the viewModel and two or more different ViewModels can access same state/data that are saved in single instance of repository
you need to read more about ViewModel to understand it better.
https://developer.android.com/topic/libraries/architecture/viewmodel
ViewModel is connected to your Activity
so you can share your Activities ViewModel only between his Fragments ,
that is what mean sharedViewModel in koin
sharedViewModel is the same if you use ViewModel Factory with same context .
sharing any data between Activities can be done via Intent , there is no another way in Android,
or you can keep some static / global data and share it between Activities
I would suggest making the app a ViewModelStoreOwner and injecting the viewModels using as owner the app.
The code required would look like this
class App : Application(), ViewModelStoreOwner {
private val mViewModelStore = ViewModelStore()
override fun getViewModelStore(): ViewModelStore {
return mViewModelStore
}
}
You can define some extensions to easily inject the viewModels
val Context.app: App
get() = applicationContext as App
inline fun <reified T : ViewModel> Context.appViewModel(
qualifier: Qualifier? = null,
noinline state: BundleDefinition? = null,
noinline parameters: ParametersDefinition? = null
): Lazy<T> {
return lazy(LazyThreadSafetyMode.NONE) {
GlobalContext.get().getViewModel(qualifier, state, { ViewModelOwner.from(app, null) }, T::class, parameters)
}
}
inline fun <reified T : ViewModel> Fragment.appViewModel(
qualifier: Qualifier? = null,
noinline state: BundleDefinition? = null,
noinline parameters: ParametersDefinition? = null
): Lazy<T> {
return lazy(LazyThreadSafetyMode.NONE) {
GlobalContext.get().getViewModel(qualifier, state, { ViewModelOwner.from(requireContext().app, null) }, T::class, parameters)
}
}
You can then inject your viewModel like this
class MainActivity : AppCompatActivity() {
private val mAppViewModel: AppViewModel by appViewModel()
}
The advantage of this solution is that you don't need to recreate the view model and if you decide to save the state between app restarts, you can easily make the app an SavedStateRegistryOwner as well and using the SavedStateHandle save/restore your state from inside the viewModel, being now bound to the process lifecycle.
I know this is very very late but you can try this:
if you are extending a baseviewmodel, you need to declare the baseViewmodel as a single then in your respective activity inject the BaseViewModel.
Practical example:
val dataModule = module {
single { BaseViewModel(get(), get()) }
}
in your ViewModel
class LoginViewModel(private val param: Repository,
param1: Pref,
param2: Engine) : BaseViewModel(param1, param2)
Then in your activity class
val baseViewModel: BaseViewModel by inject()
Hope this help someone.