I'm looking through the tutorial for Android room with a view, and trying to extend their model for using ViewModels to multiple fragments, but not really sure how.
MyApplication
class myApplication : Application() {
companion object {
var database: myDatabase? = null
var repository: myRepository? = null
}
override fun onCreate() {
super.onCreate()
database = MyDatabase.getInstance(this)
repository = MyRepository(database!!.myDatabaseDao)
}
}
MyViewModel
class MyViewModel(private val repository: MyRepository) : ViewModel() {
val allWords: LiveData<List<Words>> = repository.allWords.asLiveData()
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
class MyViewModelFactory(private val repository: MyRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return MyViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
HomeFragment
class HomeFragment : Fragment() {
private val myViewModel: MyViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
var rootView = inflater.inflate(R.layout.fragment_home, container, false)
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myViewModel.allWords.observe(viewLifecycleOwner) { words ->
// Update the cached copy of the words in the adapter.
words.let { Log.d("fragment", it.toString()) }
}
}
}
I have a couple of other fragments that will hopefully share the same ViewModel as HomeFragment. I've tried many different approaches, such as using
myViewModel = ViewModelProviders.of(activity!!).get(MyViewModel::class.java)
but all of them give me Caused by: java.lang.InstantiationException: java.lang.Class<com.example.tabtester.ViewModels.MyViewModel> has no zero argument constructor. I can't find any SO posts or documentation that shows me how to provide a constructor in Kotlin.
Also conceptually I can't find any description for what exactly is happening and how the viewmodel is being constructed (and by what). In the Room with a View tutorial, the example given is in MainActivity:
private val wordViewModel: WordViewModel by viewModels {
WordViewModelFactory((application as WordsApplication).repository)
}
This makes sense, to me; you're using the Factory to instantiate a ViewModel to use in the MainActivity. But for any description of how to use ViewModels in Fragments, I don't see where the ViewModel is being constructed. If you have multiple fragments who is constructing the ViewModel? If I use Fragments then does that mean I also need an activity to construct the ViewModel, then somehow share between the Fragments?
Would appreciate any help, or documentation that explains this more clearly.
The underlying APIs of by viewModels(), by activityViewModels() and the (now deprecated) ViewModelProviders.of() all feed into one method: the ViewModelProvider constructor:
ViewModelProvider(viewModelStore: ViewModelStore, factory: ViewModelProvider.Factory)
This constructor takes two parameters:
The ViewModelStore controls the storage and scoping of the ViewModel you create. For example, when you use by viewModels() in a Fragment, it is the Fragment which is used as the ViewModelStore. Similarly, by activityViewModels() uses the Activity as the ViewModelStore.
The ViewModelProvider.Factory controls the construction of the ViewModel if one has not already been created for that particular ViewModelStore.
Therefore if you need a custom Factory, you must always pass that Factory into all places that could create that ViewModel (remember, due to process death and recreation, there's no guarantee that your HomeFragment will be the first fragment to create your ViewModel).
private val myViewModel: MyViewModel by activityViewModels() {
MyViewModelFactory(MyApplication.repository!!)
}
As long as you're using activityViewModels(), the storage of your ViewModel will always be at the activity level, no matter what Factory you are using.
Related
I have the following implementation on my Fragment -
class HeroesDetailsFragment : Fragment() {
private val navArgs: HeroesDetailsFragmentArgs by navArgs()
private val heroesDetailsViewModel: HeroesDetailsViewModel by stateViewModel(state = { navArgs.toBundle() })
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentHeroDetailsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initListeners()
observeUiState()
observeUiAction()
}
}
My HeroesDetailsViewModel looks like this -
class HeroesDetailsViewModel(
private val savedStateHandle: SavedStateHandle,
private val heroesDetailsRepository: HeroesDetailsRepository
) : ViewModel() {
private fun getArgsModel() = HeroesDetailsFragmentArgs.fromSavedStateHandle(savedStateHandle)
init {
val navArgs = getArgsModel()
getAdditionalHeroDetails(navArgs.heroModel.id)
observeUiEvents()
}
}
And in my ViewModelModule I declare the following
val viewModelModule = module {
// ...
viewModel { params ->
HeroesDetailsViewModel(params.get(), get())
}
}
As you can see, I utilized the stateViewModel extension for Fragments that allows me to create a StateViewModel. The issue is that when trying to use the same functionality in Compose:
#Destination
#Composable
fun HeroDetailsScreen(
model: HeroesListModel,
viewModel: HeroesDetailsViewModel = getStateViewModel() //provides deprecation error
) {
}
I get the following deprecation message -
getStateViewModel will be merged to sharedViewModel - no need anymore of state parameter
I did not find any good references on this topic, and it seems weird for me because the Fragment extension stateViewModel is completely fine and not deprecated so I am missing information on what should I do to replace it.
My goal is to inject a ViewModel with state parameters that will initialize the SavedStateHandle object. Currently I am using Koin DI, will switch in the future to Dagger-Hilt so it would be also a nice bonus to see the solution both in Koin and in Dagger-Hilt.
So I finally found a way to inject the ViewModel with dynamic information coming from the Fragment. I was looking at the old Fragment / Activity way which includes handling bundles, but in Compose it's much easier as we don't need to use the SavedStateHandle object because we can handle process death by the rememberSaveable { } block, which decouples the need to inject a ViewModel with dynamic information and the need to save information for process death.
This leaves the ViewModel to only ask for the relevant model and not bother handling process death. Just pure information.
class HeroesDetailsViewModel(
heroListModel : HeroesListModel,
private val heroesDetailsRepositoryImpl: HeroesDetailsRepositoryImpl
) : ViewModel() {
init {
getAdditionalHeroDetails(heroListModel.id)
observeUiEvents()
}
}
So I added a model that will be injected when needed via the parameters field -
#Destination
#Composable
fun HeroDetailsScreen(
model: HeroesListModel,
viewModel: HeroesDetailsViewModel = koinViewModel(parameters = { ParametersHolder(mutableListOf(model)) })
) {
}
And in my DI module the implementation actually is left the same -
val viewModelModule = module {
viewModelOf(::HeroesViewModel)
viewModelOf(::HeroesListItemViewModel)
viewModel { params ->
HeroesDetailsViewModel(params.get(), get())
}
}
Hopefully this saves some time for other people in the future 💪🙂
My app uses MVVM architecture. I have a ViewModel shared by both an Activity and one of its child fragments. The ViewModel contains a simple string that I want to update from the Activity and observe in the fragment.
My issue is simple: the observe callback is never reached in my fragment after the LiveData updates. For testing, I tried observing the data in MainActivity, but that works fine. Additionally, observing LiveData variables in my fragment declared in other ViewModels works fine too. Only this ViewModel's LiveData seems to pose a problem for my fragment, strangely.
I'm declaring the ViewModel and injecting it into my Activity and Fragment via Koin. What am I doing incorrectly to never get updates in my fragment for this ViewModel's data?
ViewModel
class RFIDTagViewModel: ViewModel() {
private val _rfidTagUUID = MutableLiveData<String>()
val rfidTagUUID: LiveData<String> = _rfidTagUUID
fun tagUUIDScanned(tagUUID: String) {
_rfidTagUUID.postValue(tagUUID)
}
}
Activity
class MainActivity : AppCompatActivity(), Readers.RFIDReaderEventHandler,
RFIDSledEventHandler.TagScanInterface {
private val rfidViewModel: RFIDTagViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
rfidViewModel.rfidTagUUID.observe(this, {
Timber.d("I'm ALWAYS reached")
})
}
override fun onResume() {
rfidViewModel.tagUUIDScanned(uuid) //TODO: data passed in here, never makes it to Fragment observer, only observed by Activity successfully
}
}
Fragment
class PickingItemFragment : Fragment() {
private val rfidViewModel: RFIDTagViewModel by viewModel()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
rfidViewModel.rfidTagUUID.observe(viewLifecycleOwner, { tagUUID ->
Timber.d("I'm NEVER reached")
})
}}
Koin DI Config
val appModule = module {
viewModel { RFIDTagViewModel() }
}
In your Fragment I see you are using viewModels(). viewModels() here will be attached to the Fragment, not to the Activity.
If you want to shareViewModel between Fragment and Activity, then in Fragment you use activityViewModels(). Now, in the Fragment, your shareViewModel will be attached to the Activity containing your Fragment.
Edit as follows:
PickingItemFragment.kt
class PickingItemFragment : Fragment() {
private val rfidViewModel: RFIDTagViewModel by activityViewModels()
}
More information: Communicating with fragments
You need to use the same viewmodel, aka, sharedViewModel, the way you are doing you are using two different instances of the same viewmodel.
To fix it.
On both activity and fragment:
private val rfidViewModel: RFIDTagViewModel by activityViewModels()
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=pt-br
I am studying ViewModel to apply it to MVVM design pattern.
There was a method using by viemodels() and a method using ViewModelProvider.Factory in view model creation.
by viewModels() creates a ViewModel object.
ViewModelProvider.Factory also creates Viewmodel objects.
What is the difference between these two?
In addition, in some sample code, I saw the code in comment 3, which uses by viewModels() and factory together. What does this mean?
class WritingRoutineFragment : Fragment() {
private val viewModel: WriteRoutineViewModel by viewModels() // 1
private lateinit var viewModelFactory: WriteRoutineViewModelFactory
// private val viewModel: WriteRoutineViewModel by viewModels(
// factoryProducer = { viewModelFactory } // 3.What does this code mean?
// )
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = FragmentWritingRoutineBinding.inflate(inflater, container, false)
viewModelFactory = WriteRoutineViewModelFactory()
// viewModel = ViewModelProvider(this, viewModelFactory).get(WriteRoutineViewModel::class.java) // 2
return binding.root
}
If your ViewModel has a zero-argument constructor, or if it has a constructor where its only argument is of type Application and it's a subclass of AndroidViewModel, then you do not need a factory. (Or if your constructor is either of the above plus SavedStateHandle.) A view model factory is a class that is able to instantiate your ViewModel that has a more complicated constructor.
When instantiating your ViewModel without using a delegate, you have to use a lateinit var for the property because you can't instantiate it until onCreateView.
If your ViewModel had no need for a factory, the process of doing it without a delegate would look like this:
class WritingRoutineFragment : Fragment() {
private lateinit var viewModel: WriteRoutineViewModel
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
//...
viewModel = ViewModelProvider(this, viewModelFactory).get(WriteRoutineViewModel::class.java)
//...
}
}
and if it did need a factory, it would look like this, where you have to instantiate a factory and pass it to the ViewModelProvider constructor:
class WritingRoutineFragment : Fragment() {
private lateinit var viewModel: WriteRoutineViewModel
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
//...
viewModel = ViewModelProvider(this, WriteRoutineViewModelFactory()).get(WriteRoutineViewModel::class.java)
//...
}
}
The delegate allows you to do this more concisely in a val right at the declaration site so you don't have to do any setup of your view model property in onCreateView. It will lazily create the ViewModel the first time the property is used. The advantage is more concise and clearer code (lateinit var splits the property from its declaration and makes it mutable even though it will never change).
So the above code when no factory is needed looks like:
class WritingRoutineFragment : Fragment() {
private val viewModel: WriteRoutineViewModel by viewModels()
}
and if you do need a factory it will look like this. You pass it a function that instantiates the factory, which is easily done with a lambda:
class WritingRoutineFragment : Fragment() {
private val viewModel: WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }
}
The code in your example has an extra property just to hold the factory, which is an unnecessary complication since you'll never need to access it directly. It's also quite odd that the factory in your example has an empty constructor, because if the factory doesn't have any state, then it has no data to pass to the ViewModel constructor.
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
I'm trying to use AndroidViewModel rather than the regular ViewModel, because AndroidViewModel contains an application reference that I need for binding services.
My question is, how do I supply the application parameter to my ViewModelFactory?
At the moment I'm casting it from the ApplicationContext that the ViewModelFactory injector util already has available to it, but I've read that's dangerous because a cast of ApplicationContext may not always return the application.
I'm just learning Android, following along the Sunflower example, so the answer is probably very simple. I just don't know it . . . yet. (Hoping you can help).
Here is how I am doing it currently, which as you may notice is closely based on how Google does it in the Sunflower example:
Fragment using the AndroidViewModel
class GalleryFragment : Fragment() {
private lateinit var viewModel: GalleryViewModel
private var memoList: List<Memo> = emptyList()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.gallery_fragment, container, false)
val context = context ?: return view
val factory = InjectorUtils.provideGalleryViewModelFactory(context)
viewModel = ViewModelProviders.of(this, factory).get(GalleryViewModel::class.java)
...
Injector Util
object InjectorUtils {
fun provideMemoViewModelFactory(context: Context, memoId: Long): MemoViewModelFactory {
val application = context.applicationContext as Application //this could be dangerous
val repository = getMemoRepository(context)
return MemoViewModelFactory(application, repository, memoId)
}
}
ViewModelFactory
class MemoViewModelFactory(
private val application: Application,
private val memoRepository: MemoRepository,
private val memoId: Long
) : ViewModelProvider.NewInstanceFactory() {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MemoViewModel( application, memoRepository, memoId) as T
}
}
ViewModel
class MemoViewModel(
application: Application,
memoRepository: MemoRepository,
private val memoId: Long
) : AndroidViewModel(application) {
...
My concern is inside the InjectorUtil, where I obtain a reference to the application by casting the applicationContext. If I proceed down that path am I heading for trouble down the line? Is there somewhere else I should be adding the application reference, where I can access it directly rather than through a cast of applicationContext?
thanks
John