I have 2 viewModels -
MainViewModel**
StorageViewModel
StorageViewModel.kt
class StorageViewModel #ViewModelInject constructor(private val preferenceStorage:
PreferenceStorage, #ApplicationContext context: Context) : ViewModel() {
........
//save last played song
fun saveLastPlayedSong(song: Songs){
viewModelScope.launch {
protoDataStoreManager.saveLastPlayedSong(song)
}
}
}
Now, I want to call the saveLastPlayedSong function in MainViewModel
MainViewModel.kt
class MainViewModel #ViewModelInject constructor(
private val musicServiceConnection: MusicServiceConnection,
private val storageViewModel: StorageViewModel
) : ViewModel(){
.........
fun playOrToggleSong(
mediaItem: Songs, toggle: Boolean = false
)
{
//here, I want to call the function from StorageViewModel e.g
storageViewModel.saveLastPlayedSong(mediaItem)
}
}
How do I instantiate the "StorageViewModel" inside MainViewModel and whats the best way (Good Practice).
I'm using MVVM and Hilt.
This is usually a symptom of bad architecture.
If StorageViewModel is acting like a Repository it should not extend ViewModel. If it doesn't have connections to UI you can convert it to a repository class and that would solve your problem because it would just become an injectable singleton.
If StorageViewModel is connected to a Fragment (for example) you should take a reference to both viewmodels and pass data between them from the UI layer.
Something like:
class StorageFragment : Fragment {
private val storageViewModel: StorageViewModel by viewModels()
private val mainActivityViewModel: MainViewModel by activityViewModels()
//....
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//you can do this if the song saving is a UI related thing
//just have playOrToggleSong accept a function as parameter
//as success callback
button.setOnClickListener {
mainActivityViewModel.playOrToggleSong(...) {
storageViewModel.saveLastPlayedSong(param)
}
}
}
}
Related
I'm trying to send three values to my UserViewModel but even if sending the savedStateHandle
In my Activity I have
private val viewModel: UserViewModel by viewModels()
Then my UserViewModel is :
#HiltViewModel
internal class UserViewModel #Inject constructor(
private val myRepo: MyRepo,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
But then this savedStateHandle is empty, what I'm missing?
If you are using MVVM based on the Android Architecture guidelines you can send an event to the viewmodel from your Activity/Fragment once your view is created.
You must add savedStateHandle in AppModule. You want inject savedStateHandle.
I've been using #AssistedInject to do so as follows :
internal class UserViewModel #AssistedInject constructor(
...
#Assisted val name: String,
) : ViewModel() {
...
}
Then I had to create a Factory
#AssistedFactory
interface UserViewModelAssistedFactory {
fun create(name: String): UserViewModel
}
class Factory(
private val assistedFactory: UserViewModelAssistedFactory,
private val name: String, <-- value you want to pass
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return assistedFactory.create(name) as T
}
}
Then in the Activity/Fragment you have to inject the AssistedFactory as follows
#Inject internal lateinit var assistedFactory: UserViewModel.UserViewModelAssistedFactory
private val userViewModel: UserViewModel by viewModels {
UserViewModel.Factory(assistedFactory, intent.getStringExtra(USER_NAME_ARG).orEmpty())
}
Doing this it should work, but also your solution should work make sure you are sending the intent args correctly because it says is null looks like what you are passing is not correct, savedInstace.keys() should return everything you passed from your previous Activity/Fragment.
In my activity, I have multiple variables being initiated from Intent Extras. As of now I am using ViewModelFactory to pass these variables as arguments to my viewModel.
How do I eliminate the need for ViewModelFacotory with hilt
Here are two variables in my Activity class
class CommentsActivity : AppCompatActivity() {
private lateinit var viewModel: CommentsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
val contentId = intent.getStringExtra(CONTENT_ID_FIELD) //nullable strings
val highlightedCommentId = intent.getStringExtra(HIGHLIGHTED_COMMENT_ID_RF) //nullable strings
val commentsViewModelFactory = CommentsViewModelFactory(
contentId,
highlightedCommentId
)
viewModel = ViewModelProvider(this, commentsViewModelFactory[CommentsViewModel::class.java]
}
}
Here is my viewModel
class CommentsViewMode(
contentId : String?,
highlightedCo;mmentId : String?,
) : ViewModel() {
//logic code here
}
My app is already set up to use hilt but in this case How can I pass these 2 variables and eliminate the viewModelFactory entirely
The trick is to initialize those variables only once, while the activity can be created multiple times. In my apps, I use a flag.
View model:
class CommentsViewModel : ViewModel() {
private var initialized = false
private var contentId : String? = null
private var highlightedCommentId : String? = null
fun initialize(contentId : String?, highlightedCommentId : String?) {
if (!initialized) {
initialized = true
this.contentId = contentId
this.highlightedCommentId = highlightedCommentId
}
}
//logic code here
}
Also, you should know that there is an open issue in dagger project exactly for this capability:
https://github.com/google/dagger/issues/2287
You're welcome to follow the progress.
If you want to use hilt effectively u can follow this steps
Use #HiltViewModel in your view model
#HiltViewModel
class MyViewModel #inject constructor(private val yrParameter): ViewModel {}
Also you no longer need any ViewModelFactory! All is done for you! In your activity or fragment, you can now just use KTX viewModels() directly.
private val viewModel: MyViewModel by viewModels()
Or if you want to use base classes for fragment and activity you can use this code to pass viewModel class
abstract class BaseFragment<V: ViewModel, T: ViewDataBinding>(#LayoutRes val layout: Int, viewModelClass: Class<V>) : Fragment() {
private val mViewModel by lazy {
ViewModelProvider(this).get(viewModelClass)
}
}
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
Can you tell me if my approach is right? It works but I don't know if it's correct architecture. I read somewhere that we should avoid calling viewmodel function on function responsible for creating fragments/activities mainly because of screen orientation change which recall network request but I really need to pass arguments from one viewmodel to another one. Important thing is I'm using Dagger Hilt dependency injection so creating factory for each viewmodel isn't reasonable?
Assume I have RecyclerView of items and on click I want to launch new fragment with details - common thing. Because logic of these screens is complicated I decided to separate single viewmodel to two - one for list fragment, one for details fragment.
ItemsFragment has listener and launches details fragment using following code:
fun onItemSelected(item: Item) {
val args = Bundle().apply {
putInt(KEY_ITEM_ID, item.id)
}
findNavController().navigate(R.id.action_listFragment_to_detailsFragment, args)
}
Then in ItemDetailsFragment class in onViewCreated function I receive passed argument, saves it in ItemDetailsViewModel itemId variable and then launch requestItemDetails() function to make api call which result is saved to LiveData which is observed by ItemDetailsFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//...
val itemId = arguments?.getInt(KEY_ITEM_ID, -1) ?: -1
viewModel.itemId = itemId
viewModel.requestItemDetails()
//...
}
ItemDetailsViewModel
class ItemDetailsViewModel #ViewModelInject constructor(val repository: Repository) : ViewModel() {
var itemId: Int = -1
private val _item = MutableLiveData<Item>()
val item: LiveData<Item> = _item
fun requestItemDetails() {
if (itemId == -1) {
// return error state
return
}
viewModelScope.launch {
val response = repository.getItemDetails(itemId)
//...
_item.postValue(response.data)
}
}
}
Good news is that this is what SavedStateHandle is for, which automatically receives the arguments as its initial map.
#HiltViewModel
class ItemDetailsViewModel #Inject constructor(
private val repository: Repository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val itemId = savedStateHandle.getLiveData(KEY_ITEM_ID)
val item: LiveData<Item> = itemId.switchMap { itemId ->
liveData(viewModelScope.coroutineContext) {
emit(repository.getItemDetails(itemId).data)
}
}
we should avoid calling viewmodel function on function responsible for creating fragments/activities mainly because of screen orientation change which recall network request
Yes, in your example a request will be executed whenever ItemDetailsFragment's view is created.
Take a look at this GitHub issue about assisted injection support in Hilt. The point of assisted injection is to pass additional dependencies at object's creation time.
This will allow you to pass itemId through the constructor, which then will allow you to access it in ViewModel's init block.
class ItemDetailsViewModel #HiltViewModel constructor(
private val repository: Repository,
#Assisted private val itemId: Int
) : ViewModel() {
init {
requestItemDetails()
}
private fun requestItemDetails() {
// Do stuff with itemId.
}
}
This way the network request will be executed just once when ItemDetailsViewModel is created.
By the time the feature is available you can either try workarounds suggested in the GitHub issue or simulate the init block with a flag:
class ItemDetailsViewModel #ViewModelInject constructor(
private val repository: Repository
) : ViewModel() {
private var isInitialized = false
fun initialize(itemId: Int) {
if (isInitialized) return
isInitialized = true
requestItemDetails(itemId)
}
private fun requestItemDetails(itemId: Int) {
// Do stuff with itemId.
}
}
I using hilt to inject everything I want in viewModel, I find hilt support SavedStateHandle through #ViewModelInject, so any bundle data pass to it can be get back if I want.
class TestViewModel #ViewModelInject constructor(
private val testRepository: TestRepository,
#Assisted private val state: SavedStateHandle
) : ViewModel() {
val testItem = state["test"] ?: "defaultValue"
}
#AndroidEntryPoint
class TestFragment : Fragment() {
private val viewModel: TestViewModel by viewModels() // How to pass bundle to the init viewModel?
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentTestBinding.inflate(inflater)
binding.lifecycleOwner = this
binding.viewModel = viewModel
...
}
}
It seems like the way to use ViewModelFactory to init viewModel with bundle.
interface ViewModelAssistedFactory<T : ViewModel> {
fun create(state: SavedStateHandle): T
}
class TestViewModelFactory #Inject constructor(
private val testRepository: TestRepository
) : ViewModelAssistedFactory<TestViewModel> {
fun create(handle: SavedStateHandle) {
return TestViewModel(handle, testRepository)
}
}
class TestViewModel(
private val state: SavedStateHandle
private val testRepository: TestRepository,
) : ViewModel() {
val id = state["test"] ?: "defaultValue"
}
If I understand your question correctly, that you want in instantiate the ViewModel and pass in a bundle I suspect that Injecting ViewModel with Dagger Hilt article might help.
Scrolling towards the bottom there are 2 example using varying methods to instance a ViewModel and one in particular is passing a Bundle.
This is another good resource: No more Factory Needed for ViewModel with Dependencies.
This is what I did, maybe not elegant, but it works well:
class dataViewModel #ViewModelInject constructor(
val stateData: StateData,
#Assisted private val savedStateHandle: SavedStateHandle
): ViewModel() {
Then, to instance the viewModel you need a module definition:
#Module
#InstallIn(ApplicationComponent::class)
class StateDataModule {
#Singleton
#Provides
fun provideStateData(): StateData {
return StateData(null, null)
}
}
Then, in classes that use it I set the values and the ViewModel retains the values for all instances and across activities (which was my primary reason to do this).
dataViewModel.stateData.[property] = blah