Android MVVM: Activity with multiple Fragments - Where to put shared LiveData? - android

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.

Related

AutoClearedValue accessed from another thread after View is Destroyed

I am using AutoClearedValue class from this link and when view is destroyed, backing field becomes null and that is good but i have a thread(actually a kotlin coroutine) that after it is done, it accesses the value(which uses autoCleared) but if before it's Job is done i navigate to another fragment(view of this fragment is destroyed), then it tries to access the value, but since it is null i get an exception and therefore a crash.
what can i do about this?
also for which variables this autoCleared needs to be used? i use it for viewBinding and recyclerview adapters.
You have 2 option:
1- Cancelling all the running job(s) that may access to view after its destruction. override onDestroyView() to do it.
Also, you can launch the coroutines viewLifecycleOwner.lifecycleScope to canceling it self when view destroy.
viewLifecycleOwner.lifecycleScope.launch {
// do sth with view
}
2- (Preferred solution) Use Lifecycle aware components (e.g LiveData) between coroutines and view:
coroutines push the state or data in the live-data and you must observe it with viewLifeCycleOwner scope to update the view.
private val stateLiveData = MutableLiveData<String>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
stateLiveData.observe(viewLifecycleOwner) { value ->
binding.textView.text = value
}
}
private fun fetchSomething() {
lifecycleScope.launch {
delay(10_000)
stateLiveData.value = "Hello"
}
}

Android, Koin: How to prevent interface bound by viewModel from creating new viewModel instance?

It's hard to understand what the problem is from the headline - I'll try my best explaining:
I'm using Koin for dependency injection. I'm injecting my HomeViewModelinto my HomeFragment (the viewModel has parameters, but that should be unrelated to the problem):
// fragment code
private var viewModelParameters: ParametersDefinition? = null
lateinit var viewModel: VM
...
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, layout, container, false)
binding?.lifecycleOwner = viewLifecycleOwner
viewModel = getViewModel(HomeViewModel::class, parameters = viewModelParameters)
return binding?.root ?: inflater.inflate(layout, container, false)
}
The fragment contains a RecyclerView. The recycler's ViewHolder declares an interface, that is injected via Koins by inject()`:
class MyRecyclerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), KoinComponent {
private val callback by inject<Callback>()
fun bind(item: MyItemType) {
itemView.setOnClickListener { callback.myCallbackFunction(item) }
}
interface Callback {
fun myCallbackFunction(item: MyItemType)
}
}
My HomeviewModel implements this interface, and I bind it to the viewModel in my KoinGraph module via Koin's bind DSL method:
private val baseModule = module {
single { androidApplication().resources }
single { PermissionHelper(androidApplication()) }
...
viewModel { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
}
Now, when I click on my recycler item, the callback's myCallBackFunction is called, which should trigger the implementation in my HomeViewModel. Which it does, but: It is not the same instance, but a new HomeViewmodel.
My understanding is that Android's ViewModelclass, if used in the typical way (currently using, without Koin, by viewModels() - see here), should only exist once. But with Koin's viewModel{} call, I can create multiple instances, which I think I shouldn't be able to? Or should I (and if yes, why)?
Anyway, I'd like to bind my callback to the view model I already have (the one the fragment knows of) and not a new instance my fragment doesn't know about.
How can I achieve that using Koin and its injection patterns?
By the way, If I use
single { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
instead of
viewModel { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
my code works as intended - since I'm forcing my view model to be a singleton that way - which is what I want. But what is the point of the viewModel{} command then? And are there any downsides to it? It doesn't feel like what I should be supposed to do but maybe it's totally fine?

Does an instance of a SharedViewmodel never dies?

I have an app that has a main activity and fragments depend on it, so this is normal.
Now, two of my 10 fragments need to communicate, which I use the example given here
https://developer.android.com/topic/libraries/architecture/viewmodel.html#sharing
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
})
}
}
Now, if MasterFragment and DetailFragment dies (both does a popBackStack()) does that instance of the viewmodel keep active untill I finish the MainActivity containing this Fragments ? Because now I dont need anymore that viewmodel instance, but as per documentation says, this instance will be retained from the Activity that contains these fragments
This is not what I'm looking for to communicate between fragments since now a new instance of that viewmodel will be the same as the past one I have created, I mean, it will reuse the instance that I used with the already poped fragments, in which I will need to extra handling a deletion or reset of all the data inside this viewmodel instead of getting a new fresh viewmodel.
Does it works this way or that instance automatically dies when no fragments depending on it are in the stack anymore ?
Now, if MasterFragment and DetailFragment dies (both does a popBackStack()) does that instance of the viewmodel keep active untill I finish the MainActivity containing this Fragments ?
Correct. While it so happens that only two of your fragments use it, that ViewModel is scoped to the activity.
I mean, it will reuse the instance that I used with the already poped fragments, in which I will need to extra handling a deletion or reset of all the data inside this viewmodel instead of getting a new fresh viewmodel.
Then perhaps you should not be using activityViewModels(). For example, you could isolate these two fragments into a nested navigation graph and set up a viewmodel scoped to that graph.
Does it works this way or that instance automatically dies when no fragments depending on it are in the stack anymore ?
The ViewModel system does not know about what is or is not "depending on it". It is all based on the ViewModelStore and the ViewModelStoreOwner that supplies it. activityViewModels() uses the activity as the ViewModelStoreOwner, so viewmodels in that ViewModelStore are tied to the activity.

Sharing one view model with multiple fragments

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
})
}
}

How to use android navigation without binding to UI in ViewModel (MVVM)?

I am using android navigation that was presented at Google I/O 2018 and it seems like I can use it by binding to some view or by using NavHost to get it from Fragment. But what I need is to navigate to another specific view from ViewModel from my first fragment depending on several conditions. For ViewModel, I extend AndroidViewModel, but I cannot understand how to do next. I cannot cast getApplication to Fragment/Activity and I can't use NavHostFragment. Also I cannot just bind navigation to onClickListener because the startFragment contains only one ImageView. How can I navigate from ViewModel?
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
showTour()
}else{
showCodeFragment()
Log.d(TAG, "Show Code Fragment")
}
}
}
private fun showCodeFragment(){
//??
}
private fun showTour(){
//??
}
}
My Fragment
class CaptionFragment : Fragment() {
private lateinit var viewModel: CaptionViewModel
private val navController by lazy { NavHostFragment.findNavController(this) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
viewModel = ViewModelProviders.of(this).get(CaptionViewModel::class.java)
return inflater.inflate(R.layout.fragment_caption, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.start()
}
}
I want to keep logic of navigation in ViewModel
How can I navigate from ViewModel?
The answer is please don't. ViewModel is designed to store and manage UI-related data.
New Answer
In my previous answers, I said that we shouldn't navigate from ViewModel, and the reason is because to navigate, ViewModel must have references to Activities/Fragments, which I believe (maybe not the best, but still I believe it) is never a good idea.
But, in recommended app architecture from Google, it mentions that we should drive UI from model. And after I think, what do they mean with this?
So I check a sample from "android-architecture", and I found some interesting way how Google did it.
Please check here: todo-mvvm-databinding
As it turns out, they indeed drive UI from model. But how?
They created an interface TasksNavigator that basically just a navigation interface.
Then in the TasksViewModel, they have this reference to TaskNavigator so they can drive UI without having reference to Activities / Fragments directly.
Finally, TasksActivity implemented TasksNavigator to provide detail on each navigation action, and then set navigator to TasksViewModel.
You can use an optional custom enum type and observe changes in your view:
enum class NavigationDestination {
SHOW_TOUR, SHOW_CODE_FRAGMENT
}
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
private val _destination = MutableLiveData<NavigationDestination?>(null)
val destination: LiveData<NavigationDestination?> get() = _destination
fun setDestinationToNull() {
_destination.value = null
}
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
_destination.value = NavigationDestination.SHOW_TOUR
}else{
_destination.value = NavigationDestination.SHOW_CODE_FRAGMENT
Log.d(TAG, "Show Code Fragment")
}
}
}
}
And then in your view observe the viewModel destination variable:
viewModel.destination.observe(this, Observer { status ->
if (status != null) {
viewModel.setDestinationToNull()
status?.let {
when (status) {
NavigationDestination.SHOW_TOUR -> {
// Navigate to your fragment
}
NavigationDestination.SHOW_CODE_FRAGMENT -> {
// Navigate to your fragment
}
}
})
}
If you only have one destination you can just use a Boolean rather than the enum.
There are two ways I can recommend doing this.
Use LiveData to communicate and tell the fragment to navigate.
Create a class called Router and this can contain your navigation logic and reference to the fragment or navigation component. ViewModel can communicate with the router class to navigate.

Categories

Resources