I am having some common logic which I currently have it in a Util class. Now, I want to move this logic to ViewModel class. As this util method is used in different fragments, is it a good practice to create a common view model (feature based view model) for multiple fragments. I know Google recommended to use 1 view model for 1 view. Please suggest.
If you've got common code, you could have several viewModels that inherit from a baseViewModel, which contains the shared code.
The advantage of this over a Util class is that the shared code is only visible to ViewModels that derive from the base, and can't get confused with anything else.
It's better to create a viewmodel per each fragment, but it is possible to create a single viewmodel for several fragments. According to the official documents:
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
class DetailFragment : Fragment() {
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
// Update the UI
})
}
}
Related
What I would like to do is to remove at each fragment such lines:
val retrofitService = RetrofitService.getInstance(requireContext())
val mainRepository = MainRepository(retrofitService)
viewModel = ViewModelProvider(
this#SomeFragment,
AppVMFactory(mainRepository)
)[AppViewModel::class.java]
all these lines can be placed in onCreateView or some other lifecycle methods. I have a lot of fragment, so as a result a lot of such code scopes. I think it is not logical to have similar code inside every file. At first I thought about DI, but as I got to know (maybe I'm wrong) that it is not possible to use DI with ViewModel where I have some constructor parameters. So, I started thinking about creating some single CoreFragment or something like that for having such scope and then make implementation of this CoreFragment inside all other fragment. Let see some example: we have such SampleFragment with lines which I would like to optimize:
class SomeSampleFrg : Fragment(R.layout.fragment_sample) {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewBinding = FragmentSampleBinding.bind(view)
val retrofitService = RetrofitService.getInstance(requireContext())
val mainRepository = MainRepository(retrofitService)
viewModel = ViewModelProvider(
this#SampleFragment,
AppVMFactory(mainRepository)
)[AppViewModel::class.java]
....
}
...
}
then I created CoreFragment:
open class CoreFragment: Fragment() {
lateinit var viewModel: AppViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val retrofitService = RetrofitService.getInstance(requireContext())
val mainRepository = MainRepository(retrofitService)
viewModel = ViewModelProvider(
this#CoreFragment,
AppVMFactory(mainRepository)
)[AppViewModel::class.java]
}
}
and then the field viewModel will be accessible after implementation of this fragment class. Is it possible to do? Or it is very stupid question?)) The only problem which I can not even imagine how to do in addition is how to work with DataBinding in such situation) I mean that I will need to make some more fields and also pass layout to this CoreFragment. Maybe someone has any ideas how to do it?
I am using a Room Database with AndroidViewModel (without Factory)
class RoomModel(app: Application) : AndroidViewModel(app) {
// ....
}
and not sure about how to correctly initialize that and also, if I could use one-time initilization for whole app.
I have two ways to deal with initializations which seem to work ok:
Initializing in onViewCreated():
...
private lateinit var database: RoomModel
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
database = ViewModelProvider(this)[RoomModel::class.java]
...
}
Initializing by activityViewModels():
...
private val mData: RoomModel by activityViewModels()
...
The both seem to work, but not sure which one would never lead to an app crash at some point.
Also, I would like to know, if it's a good practce using one shared reference variable of my RoomModel declared and initialized in a base Fragment that is used by other fragments like this:
class BaseFragment : Fragment() {
...
lateinit var database: RoomModel
// Or
val database: RoomModel by activityViewModels()
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
database = ViewModelProvider(this)[RoomModel::class.java]
...
}
}
Some other fragments extended by BaseFragment() like this:
class FragmentA : BaseFragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
// use the reference variable database to observe or add or edit data
}
}
and
class FragmentB : BaseFragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
// use the reference database to observe or add or edit data
}
}
and so on...
In this way I have only to init the Room once and use it without reinitializing in each fragmet I need to access to. Is that a goog idea or I rather need to use individual declaration and initialization of RoomModel in each Fragment I need access to?
Many thanks in advance.
Your Room Database instance should be a singleton. The instance should live as long as the application itself. It is costly to initialize it every time you need to use it. You should initialize Room Database in the application and each ViewModel that needs Room Database instance will get it from the application as a dependency. Or you can provide the Room Database as a dependency to your ViewModel, but then you need to create a factory for your ViewModel. You can also use a Dependency Injection library to manage it for you, but it can be overwhelming at first.
Without using any dependency injection library, you can see the example below.
Override the Application, and don't forget to declare it in the AndroidManifest.xml file.
AndroidApp.kt
class AndroidApp : Application() {
lateinit var myRoomDatabase: MyRoomDatabase
override fun onCreate() {
super.onCreate()
myRoomDatabase = Room
.databaseBuilder(applicationContext, MyRoomDatabase::class.java, "my-room-database")
.build()
}
}
AndroidManifest.xml
...
<application
android:name=".AndroidApp"
...
>
...
When you extends the AndroidViewModel you have the Application instance as a dependency. Cast it to AndroidApp and now you can get myRoomDatabase.
MainViewModel.kt
class MainViewModel(app:Application) : AndroidViewModel(app) {
private val myRoomDatabase = (app as AndroidApp).myRoomDatabase
...
}
FragmentAViewModel.kt
class FragmentAViewModel(app:Application) : AndroidViewModel(app) {
private val myRoomDatabase = (app as AndroidApp).myRoomDatabase
...
}
FragmentBViewModel.kt
class FragmentBViewModel(app:Application) : AndroidViewModel(app) {
private val myRoomDatabase = (app as AndroidApp).myRoomDatabase
...
}
I have fragment which contain two dialog fragments i.e (Fragment A -> (Navigate) -> (Dialog A, DialogB) I want to share data between these fragments I tried this way mentioned in developer android Share data between a parent and child fragment
class ListFragment: Fragment() {
// Using the viewModels() Kotlin property delegate from the fragment-ktx
// artifact to retrieve the ViewModel
private val viewModel: ListViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.filteredList.observe(viewLifecycleOwner, Observer { list ->
// Update the list UI
}
}
}
class ChildFragment: Fragment() {
// Using the viewModels() Kotlin property delegate from the fragment-ktx
// artifact to retrieve the ViewModel using the parent fragment's scope
private val viewModel: ListViewModel by viewModels({requireParentFragment()})
...
}
When I use it in my case is not working and the views in the parent fragment are not changing when I modify data in the child fragment Also when I logged the view models I found that each view model have its own Id as shown below. Any way to deal with this problem or clear explanation to this.
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 💪🙂
I have an architectural question about the android ViewModels:
Let's say that in my App I have an Activity with two Fragments inside (using a Viewpager). The two fragments do different things (therefore may have their own ViewModel?), but they also both need various data that is similar.
This is for example the state if a network connection is available or not (and both fragments show different error UIs if there is no connection), or some user setting that comes via a Push from a server and affects both fragments equally.
This looks something like this:
Now my question is how to deal with that situation when using ViewModels?
Is it good that a view observes multiple ViewModels, like it would be if I have a ViewModel for the Activity (holding the state that both need equally) and one for each fragment, like this:
This was hinted here for example, but it is not a good practice, as the relationship in MVVM generally is
View n - 1 ViewModel n - 1 Model
But I am not sure where the right place for such shared LiveData is in my case?
Late answer but I asked myself the same question and found the answer in Google guide.
Especially for fragments, it is mentioned on Google Documentations explicitly here
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
class DetailFragment : Fragment() {
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
// Update the UI
})
}
}
I think the concept behind the ViewModel was that it is supposed to be related to a single "Screen" rather than a "View". So going by that logic, I think you can use the same ViewModel if multiple fragments reference the same ViewModel because they technically belong to the same "Screen".
In the fragments, you could request the activity for the ViewModel which holds the instance of LiveData and could give you the updates as needed.
Hope this answers your question.
Update: I found a link to a sample fragment in Google samples. Check out onCreateView() method. Pasting code below for reference:
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View root = inflater.inflate(R.layout.addtask_frag, container, false);
if (mViewDataBinding == null) {
mViewDataBinding = AddtaskFragBinding.bind(root);
}
mViewModel = AddEditTaskActivity.obtainViewModel(getActivity());
mViewDataBinding.setViewmodel(mViewModel);
setHasOptionsMenu(true);
setRetainInstance(false);
return mViewDataBinding.getRoot();
}
P.S. If you have found a better solution/answer/practice, lemme know.