How to share same instance of ViewModel between Activities using Koin DI? - android

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.

Related

How to share viewmodel between two fragments which belong to two different activities

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

How to pass arguments to ViewModel from Fragment and inject dependencies at the same time?

I need to open a Compose component with its own ViewModel and pass arguments to it, but at the same time I inject dependencies to this ViewModel. How can I achieve this? Can I combine ViewModel factory and Dependency Injection (Hilt)?
Yes. you can..
Have your component be like this:
#Composable
fun MyScreen(
viewModel: MyViewModel = hiltViewModel()
) {
...
}
and in your viewModel:
#HiltViewModel
class MyViewModel #Inject constructor(
private val repository: MyRepository,
... //If you have any other dependencies, add them here
): ViewModel() {
...
}
When you pass arguments to the ViewModel, make sure that Hilt knows where to get that dependency. If you follow the MVVM architecture, then the ViewModel should handle all the data and the composable all the ui related components. So usually, you only need the ViewModel injection into the composable and all the other data injected dependencies into the ViewModel.
The composable should only care about the data that it gets from the ViewModel. Where the ViewModel gets that data and the operations it does on that data, it does not care.
Lemme know if this is what you meant..
Check out the official website for more:
Hilt-Android
Yes, you can. This is called "Assisted Inject" and it has it's own solutions in Hilt, Dagger(since version 2.31) and other libraries like AutoFactory or square/AssistedInject.
In this article, you can find an example of providing AssistedInject in ViewModel for Composable with Hilt Entry points.
Here is some code from article in case if article would be deleted:
In the main Activity, we’ll need to declare EntryPoint interface which will provide Factory for creating ViewModel:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
#EntryPoint
#InstallIn(ActivityComponent::class)
interface ViewModelFactoryProvider {
fun noteDetailViewModelFactory(): NoteDetailViewModel.Factory
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NotyTheme {
NotyNavigation()
}
}
}
}
We get Factory from Activity and instantiating our ViewModel with that Factory and assisted some field:
#Composable
fun noteDetailViewModel(noteId: String): NoteDetailViewModel {
val factory = EntryPointAccessors.fromActivity(
LocalContext.current as Activity,
MainActivity.ViewModelFactoryProvider::class.java
).noteDetailViewModelFactory()
return viewModel(factory = NoteDetailViewModel.provideFactory(factory, noteId))
}
Now just go to your navigation components and use this method to provide ViewModel to your Composable screen as following:
NavHost(navController, startDestination = Screen.Notes.route, route = NOTY_NAV_HOST_ROUTE) {
composable(
Screen.NotesDetail.route,
arguments = listOf(navArgument(Screen.NotesDetail.ARG_NOTE_ID) { type = NavType.StringType })
) {
val noteId = it.arguments?.getString(Screen.NotesDetail.ARG_NOTE_ID)!!
NoteDetailsScreen(navController, noteDetailViewModel(noteId))
}
}

share data between fragments using ModelFactory

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

Why do we need ViewModelProvider.Factory to pass view model to a screen?

I am new to Android development. Currently, I am using Jetpack Compose to build Android apps. I am also learning with MVVM architecture.
One thing I don't understand with this architecture is why we need to use ViewModelProvider.Factory to pass view model to a screen.
For example,
Instead of this,
#Composable
fun HomeScreen() {
val factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val repository = InMemoryPlantService()
#Suppress("UNCHECKED_CAST")
return HomeViewModel(
plantRepository = repository
) as T
}
}
val homeViewModel: HomeViewModel = viewModel(null, factory)
val currentState: State<HomeViewState> = homeViewModel.viewState.collectAsState()
HomeScreenScaffold(currentState.value)
}
Can't we do this,
#Composable
fun HomeScreen() {
val repository = InMemoryPlantService()
val homeViewModel: HomeViewModel = HomeViewModel(
plantRepository = repository
)
val currentState: State<HomeViewState> = homeViewModel.viewState.collectAsState()
HomeScreenScaffold(currentState.value)
}
Please help.
Full source code can be found here: https://github.com/adammc331/bloom
HomeScreen can be found here: https://github.com/AdamMc331/Bloom/blob/development/app/src/main/java/com/adammcneilly/bloom/HomeScreen.kt
When you call:
val homeViewModel: HomeViewModel = viewModel(null, factory)
The function viewModel(...) will create a new HomeViewModel if it's the first time you request the ViewModel, or it will return the previous instance of HomeViewModel if it already exists. That's one of the advantages of using ViewModels, because on configuration change (or on recomposition) your ViewModel should be reused, not created again. And the way it works is by using a ViewModelProvider.Factory to create the ViewModel when it's necessary. Your ViewModel has a parameter on its constructor, there's no way the default Android classes would know how to create your ViewModel and pass that parameter (i.e. the repository) without you providing a custom ViewModelProvider.Factory. If your ViewModel doesn't have any parameters, the default ViewModelProvider.Factory uses reflection to create your class by using the no-argument constructor.
If you do this:
val homeViewModel: HomeViewModel = HomeViewModel(
plantRepository = repository
)
Your ViewModel will be created many times and won't be reused across configuration changes or recompositions because you're always creating it there - instead of asking for it to be created or reusing it if it already exists, which is what the viewModel(...) function does.
As per a codelab in Room,
By using viewModels and ViewModelProvider.Factory,the framework will take care of the lifecycle of the ViewModel. It will survive configuration changes and even if the Activity is recreated, you'll always get the right instance of the WordViewModel class.
You do not have to use ViewModelProvider.Factory to instantiate your ViewModel.
Lets assume you have an Entity:
#Entity(tableName = "user")
data class User(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "user_id") val userId: Long)
And a DAO for that entity:
#Dao
interface UserDao {//some methods}
Without using a repository you can instantiate your ViewModel with the help of android.app.Application like so:
class UserViewModel(
application: Application
) : AndroidViewModel(application) {
val dao = AppDatabase.getDatabase(application, viewModelScope).userDao()
}
And then later in a Fragment create your ViewModel which you can later pass into your composable:
private val userViewModel: userViewModel by viewModels()

Clearing sharedViewModel

I am using Koin for injecting viewModel into fragment. My app is single activity. I need that sharedViewModel only in servisFragment and partFragment.
I would like to clear that viewModel from Activity after navigation marked with red.
How can I do that?
Code for injecting viewModel
private val servisViewModel by sharedViewModel<ServisViewModel>()
Koin sharedViewModel
inline fun <reified T : ViewModel> Fragment.sharedViewModel(
name: String? = null,
noinline from: ViewModelStoreOwnerDefinition = { activity as
ViewModelStoreOwner },
noinline parameters: ParametersDefinition? = null
) = lazy { getSharedViewModel<T>(name, from, parameters) }
Thank you for any help.
if you need to clear all viewModels from that Fragment try this in your Fragment
viewModelStore.clear()
if you need to clear concrete ViewModel try this
getViewModelStore(ViewModelParameters(...)).clear()
If you are using koin to inject, in the onDestoy of the fragment you should use
requireActivity().viewModelStore.clear()
because viewModelStore directly from fragment will return none to clear
But the problem with this is that it will clear ALL the view model scoped within this ViewModelStore. So you won't have control of which ViewModel to clear.

Categories

Resources