Q&A Getting separate instances of ViewModel across different fragments - android

I have two fragments. One fragment (InterfaceFragment) displays a spinner, which displays a list of strings (groupNames). I have this linked up to MutableLiveData<List> in my ViewModel. This sets the value of the MutableLiveData from a Room database. This all works and displays great.
Simplifying slightly, this main fragment can navigate to another fragment (EditGroupFragment) which can add or remove these groupNames from the database. Now of course I would like that when I navigate back it would automatically update the spinner's contents, however it doesn't. I am using the same viewmodel in both fragments, however I believe they are using two different instances of the viewmodel, so when the viewmodel in EditGroupFragment changes the value, this calls the onChanged() listener of the observers since I am re-installing them, however the value returned is outdated.
When the InterfaceFragment's onCreateView() is called, it re-initialises the viewmodel, and the spinner items are updated with the current correct items.
A simple solution is manually re-initialising the desired values in the viewmodel in the InterfaceFragment's onResume() method, however I feel this isn't the 'correct' solution, there is clearly something going wrong.
I believe I would need a way that changing the MutableLiveData value in EditGroupFragment causes the observers I set up in InterfaceFragment to call onChanged() and update the UI.
Different instances of the viewmodel:
/*
I/EditDictGroupFrag: dictViewModel = com.example.mydictionaryapp.dictionary.DictionaryInterfaceViewModel#8326975
DictInterface: dictViewModel = com.example.mydictionaryapp.dictionary.DictionaryInterfaceViewModel#80e8488
//I set the value in EditGroupFragment
I/onGetAllDictGroups: allDictGroups.value set to : [Test Group]
//I check what the value is in my InterfaceFragment
DictInterface : dictViewModel.allDictGroups.value = [Test Group 1, Test Group 2]
*/
Okay time for some code.
This is how I'm getting the viewmodel in each Fragment. (I don't wanna post all the other irrelevant code)
class DictionaryInterfaceFragment: Fragment() {
private lateinit var dictViewModel: DictionaryInterfaceViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
dictViewModel = ViewModelProvider(this).get(DictionaryInterfaceViewModel::class.java)
dictViewModel.dictGroupNames.observe(viewLifecycleOwner, Observer {
allDictGroups -> // set spinner items
}
}
}

So while writing out the question I stumbled upon a solution. By passing the activity into the ViewModelProvider both fragments can gain access to the same viewmodel, rather than the provider supplying different instances to each fragment.
eg.
dictViewModel = ViewModelProvider(requireActivity()).get(DictionaryInterfaceViewModel::class.java)
Please feel free to contribute anything else to the discussion, and I hope this helps someone.

Related

Pass value of a MutableLiveData of ViewModels

I'm very very new to Android and I'm trying to understand/integrate Data Binding.
I think I'm missing something since it always displays null
Here is my ViewModel
class LockDetailsModel: ViewModel() {
var name = MutableLiveData<String>()
fun setName(name: String) {
this.name.value = name
}
In my LockActivity, I set the value of name based on a certain field.
val viewModel: LockDetailsModel by viewModels()
viewModel.setName(unitName.toString())
And in my other activity, which displays the layout, this is what I did to bind the model
val viewModel: LockDetailsModel by viewModels()
binding: ActivityConnectLockBinding = DataBindingUtil.setContentView(this, R.layout.activity_connect_lock)
binding.lockDetails = viewModel
binding.lifecycleOwner = this
It displays null :(
Thanks in advance for your help!
The reason behind you getting null value is because you are using different viewModel instances in both the activities , thus the value stored by you in first Activity cannot be obtained in the Second activity . You need to have SharedViewModel between both the activities , which is not possible due to Single Activity principle . So to implement logic that you want to implement you need to have Fragments and make use of fragment-ktx library to easily create SharedViewModel .If you want an example you can refer to my answer here :
Want to show progress bar while moving to the previous page from the current page
The above answer describes how to get progress of a download started in one activity into another .

retreiving data from Room as LiveData does not trigger observer in some cases only

In my app i use the Room library to handle user data, all the functionality has been implemented like in the "Android Basics in Kotlin" Tutorial Unit 5 on developer.android.com.
In one fragment i need to fetch a single item out of the database - for that i implemented the function in the fragment's viewmodel:
fun retrievePlaceItem(id: Int): LiveData<PlaceItem> {
return itemDao.getPlaceItem(id).asLiveData()
}
the ItemDao is passed into the ViewModel Factory from the Room Database instance, which itself is instantiated in the custom Application class.
this is the query used in the ItemDao interface:
#Query("SELECT * FROM placeItem WHERE id = :id")
fun getPlaceItem(id: Int): Flow<PlaceItem>
Data in the ItemDao is returned as a Flow, and turned into LiveData in the fetching function.
The Fragment itself observes the return of the function with a passed id, and when the observer triggers, the value is stored in a lateinit var of the corresponding datatype.
lateinit var placeItem: PlaceItem
...
override fun onViewCreated(...) {
super.onViewCreated(view, savedInstanceState)
...
val id = navigationArgs.itemId
sharedViewModel.retrievePlaceItem(id)
.observe(this.viewLifecycleOwner) { selectedItem ->
placeItem = selectedItem
}
...
}
this works flawlessly, the item is retrieved, the observer gets triggered, and the lateinit var placeItem is initialized for further use.
in another fragment, that follows later on, i use a different viewmodel with the exact same function - i try to retrieve the value in the exact same way, observing the function return within the onViewCreated method of the fragment. the code is exactly the same, and i tried comparing it to the things taught in the tutorial - no deviations whatsoever. when i now go to use the value, i get an error
kotlin.UninitializedPropertyAccessException: lateinit property placeItem has not been initialized
after inspecting my code using logs, i understood the following:
the viewmodel function to retrieve the item is called
the correct item id is used
the code inside the observer curly brackets is not executed
i tried using the same viewmodel in both fragments, anything until there was no more conceivable difference between these two pieces of code. yet the first one works, the second one doesn't. something in my code creates a difference between the two instances of me using the database to fetch an item.
I avoided the problem by moving the other functions, that will handle the lateinit var, into the observer brackets. i'm unsure as to why that was not needed in my other fragment, but this works just fine!

How to transfer data from one Fragment to another?

I am writing an application on Kotlin (Android Studio), using jetpack.navigation architecture.
There are two fragments: The first contains a list with class instances, which I display in the RecyclerView, the second for EditText (I fill in the client data). I also use Livedata and ViewModel.
The problem is that when I go to the second fragment, fill in the data and confirm, I go to the 1st fragment. As I understand it, the following lines destroy the old Fragment1, and create a new one. the list on the first fragment is reset to zero (although the list is saved when you rotate the screen and minimize the application).
val client = Clients(id,name,secondName,thirdName, address, creditCard, bankNum)
val action = Fragment2Directions.actionFragment2ToFragment1(client)
findNavController().navigate(action)
I could not find how to solve problem using the navigation component. I will be very grateful.
To pass data between two fragments with jetpack navigation you have to use Safe Args
pass an argument section like
<fragment android:id="#+id/myFragment" >
<argument
android:name="myArg"
app:argType="integer"
android:defaultValue="0" />
</fragment>
add classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" in top level gradle file
and add the plugin apply plugin: "androidx.navigation.safeargs.kotlin"
now send the value like so
override fun onClick(v: View) {
val amountTv: EditText = view!!.findViewById(R.id.editTextAmount)
val amount = amountTv.text.toString().toInt()
val action = SpecifyAmountFragmentDirections.confirmationAction(amount)
v.findNavController().navigate(action)
}
and receive it as
val args: ConfirmationFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val tv: TextView = view.findViewById(R.id.textViewAmount)
val amount = args.amount
tv.text = amount.toString()
}
However safeargs works only for primitive types so you have to deconstruct and reconstruct if you're trying to pass Objects
As you are using ViewModels, I recommend that you use a shared ViewModel. The way it works is that multiple Fragments within the same Activity have access to the same ViewModel instance.
There is an example on Android Developers that fits your use case exactly. It shows how to use a shared ViewModel to do this using the master-detail navigation pattern and LiveData. I recommend you take a look at it.
Why not to use Safe Args here: You can try using Safe Args to achieve what you are trying to achieve, but I strongly recommend against it: You would have to deal with somehow using Safe Args to pass your Client objects between the Fragments back and forth (which means either sending each field individually or bundling) and you would have to manually update your LiveData objects - which defeats the purpose of LiveData. Using a shared ViewModel, you do not have to worry about any of that. No sending data back and forth, no taking your Client objects apart or bundling, no manual updating of LiveData objects - you simply access the same LiveData instance from both Fragments through your ViewModel.

How I can prevent reloading data second time after configuration change using ViewModel?

Reloading data after every rotation
I fetch data in onCreate and observe in onCreateView().
I want to know after rotating the phone(or after configuration changes data is reloaded again as a result I have these logs before rotation
fetchConfig ->observe
and after rotating
I have
observe ->fetchConfig ->observe
How I can prevent reloading data second time?
I have added in fetchConfig()
if(customerConfigData.value==null) {}
but I am not sure is it the best solution
private val viewModel: HomeViewModel by lazyViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.fetchConfig()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
viewModel.customerConfigData.observe(viewLifecycleOwner, Observer {
Log.i("test","observe")
})
return inflater.inflate(R.layout.fragment_home,container,false)
}
fun fetchConfig() {
Log.i("test","fetchConfig")
uiScope.launch {
val configEndpoint = EnigmaRiverContext.getExposureBaseUrl().append("v1/customer").append(AppConstants.CUSTOMER_UNIT)
.append("businessunit").append(AppConstants.BUSINESS_UNIT).append("defaultConfig").append("?preview=true")
val parsedData = homeRepository.fetchConfig(configEndpoint, GetConfigCall())
customerConfigMutableData.postValue(parsedData)
}
}
One solution I think would be to move call to fetchConfig() in to the init block of your ViewModel
As you can see, your method has a parameter called savedInstanceState: Bundle?. This bundle is able to save the state of the app when the configuration changes. So, you can put here any flag you want.
Now, remember that ViewModels are designed to be implemented with a good code base. So, you need to separate the Ui layer from the business layer. The fetch configuration method should be in another class which doesn't depend on the Android lifecycle. I strongly recommend reading these articles.
https://medium.com/androiddevelopers/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders-fc7cc4a6c090
https://developer.android.com/jetpack/docs/guide
In conclusion. Your solution is not the best. The best approach is to implement a correct layer for fetching the info in a way that it doesn't depend on the Android lifecycle.
I too had similar issue. I was suggested to try Event wrapper for LiveData, it had solved my problem:)
Here's the link:
How to stop LiveData event being triggered more than Once
Hope this helps!

How to correctly process lifecycle of fragment if app is being killed in background?

class MyFragment : BaseFragment {
private lateinit var myPresenter: Contract.MyPresenter
override lateinit var adapter: MyAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myPresenter.view = this
adapter = MyAdapter(
myPresenter,
this,
this
)
recyclerView.adapter = adapter
}
I've structured this type of classes in alot of places in my application. I found out that users get alot of crashes and they're usually UninitializedPropertyAccessException. Before you say, that I should make my variable nullable, I think the problem is bigger.
This works fine if u do simple testing - open app and test. Well, if u open the same fragment, press home, and then kill the application process and later on try to re-open the application, it gives error to lateinit variables. How should we by-pass this error? I use MVP and MVVM in my applications, so it means that it's crucial to save Presenters or ViewModels with it's data, because, when process is killed, the data is lost and later on when user tries to interact with application - if the variable is set to lateinit - we get an error. If we set it to be nullable, we won't get the error, but still, we will have unexpected app behaviour. We do not want that...
What's the correct way (examples would be great) to save all UI related things (recyclerviews, adapter instances ect..) WITH ViewModel / Presenter instances?

Categories

Resources