How to lazily save ViewModel's SavedStateHandle? - android

I have a screen that loads a bunch of requests and collects some data from the user on the same screen and an external WebView. Therefore, I have a ViewModel that contains these complex request objects (+ user input data). I need to persist this data through system-initiated process death, which SavedStateHandle is designed for. But I don't want to persist this data in a database because it is only relevant to the current user experience.
I have integrated my ViewModels with Hilt and received SaveStateHandle. Because I have some complex objects that are accessed/modified in several places in code I can't save them "on the go". I made them implement Parcelable and just wanted to save them at once. Unfortunately, ViewModels don't have a lifecycle method like onSaveInstanceState().
Now, I have tried using onCleared() which sounded like a ok place to write to the handle. But it turns out that all .set() operations I perform there get lost (I'm testing this with developer options "Don't keep activities". When I use .set() elsewhere, it does work). Because the ViewModel is not tied to the lifecycle of a single fragment/activity but rather to a NavGraph I can't call in from their onSaveInstanceState().
How/where can I properly persist my state in SaveStateHandle?

This is precisely the use case that the Lifecycle 2.3.0-alpha03 release enables:
SavedStateHandle now supports lazy serialization by allowing you to call setSavedStateProvider() for a given key, providing a SavedStateProvider that will get a callback to saveState() when the SavedStateHandle is asked to save its state. (b/155106862)
This allows you to handle any complex object and get a callback exactly when it needs to be saved.
var complexObject: ComplexObject? = null
init {
// When using setSavedStateProvider, the underlying data is
// stored as a Bundle, so to extract any previously saved value,
// we get it out of the Bundle, if one exists
val initialState: Bundle = savedStateHandle.get<Bundle?>("complexObject")
if (initialState != null) {
// Convert the previously saved Bundle to your ComplexObject
// Here, it is a single Parcelable, so we'll just get it out of
// the bundle
complexObject = initialState.getParcelable("parcelable")
}
// Now to register our callback for when to save our object,
// we use setSavedStateProvider()
savedStateHandle.setSavedStateProvider("complexObject") {
// This callback requires that you return a Bundle.
// You can either add your Parcelable directly or
// skip being Parcelable and add the fields to the Bundle directly
// The key is that the logic here needs to match your
// initialState logic above.
Bundle().apply {
putParcelable("parcelable", complexObject)
}
}
}

Adding to #ianhanniballake, you don't need to add any data to Bundle. You can still access Parcelable (or another data type) directly. The callback still works when it needs to save it.
init {
savedStateHandle.setSavedStateProvider("") {
savedStateHandle["complexState"] = state
Bundle()
}
}
var state by mutableStateOf(
savedStateHandle["complexState"] ?: ComplexState()
)

Related

Should i save my User Details in Viewmodel as well as SharedPrefernces?

Should i update my User Details in Viewmodel as well as SharedPrefernces?
For example, i have fields like
name
age
currentSalary
organisation
+8 more
I have few doubts now:
Am i supposed to create LiveData of each of these fields?
Also, i have to save them in Sharedprefernces too. So doesn't this feel redundant? First, saving it in Viewmodel and then saving it in Sharedprefernces.
When i move from screen 1 to screen 2, should i fetch user's name from ViewModel or the api?
MainViewModel.kt Sample Code
private val _experience = MutableLiveData<String>()
val experience : LiveData<String>
get() = _experience
private val _name = MutableLiveData<String>()
val name : LiveData<String>
get() = _name
private val _isLoggedIn = MutableLiveData<Boolean>()
val isLoggedIn : LiveData<Boolean>
get() = _isLoggedIn
fun setName(name: String){
_name.value = name
}
fun setExperience(exp: String){
_experience.value = exp
}
fun logIn(){
_isLoggedIn.value = true
}
MyFragment
binding.btnSubmit.setOnClickListener {
val name = binding.etName.text.toString()
val email = binding.etEmail.text.toString()
val age = binding.etAge.text.toString()
val org = binding.etOrg.text.toString()
//saving in viewmodel
mainViewModel.setName(name)
mainViewModel.setCurrentOrganisation(org)
mainViewModel.setEmail(email)
mainViewModel.setAge(Integer.valueOf(age))
//saving in sharedpreference
editor.putInt("age", Integer.valueOf(age))
editor.putString("name", name)
editor.putString("email", email)
editor.putString("org", org)
}
To me, My fragment looks a lot of lines of codes. I don't know if i am using the right approach.
The ViewModel is meant to sit between your View layer (the UI) and the Data layer (the core app functionality, stored data etc). A ViewModel acts as a go-between, passing data to the UI for display, and translating UI events to function calls in the app.
The ViewModel's state is transient - in Android it sticks around long enough to survive things like Activity recreation. If you use the SavedStateHandle component, you can store its running state so it can be rebuilt if the app is destroyed in the background - but this explicitly won't survive the app being closed and restarted. The ViewModel isn't about persisting data, just about the current, temporary state of things.
So actually storing your data is part of the data layer. That's where the SharedPreferences comes in - but you could be storing (and reading) data using all kinds of storage, even across a network. The ViewModel's role is to access that data, and expose it to the UI somehow - possibly even transforming that data into a more suitable form for the UI to consume.
So while you might have duplication going on, there's a reason for it. SharedPreferences is there to actually store and persist the data. LiveData is just there for the UI to see what it should be displaying. They may or may not be the same thing!
Have a look at this Android guide on designing your app architecture - it goes into the theory behind how you organise things, and how the data flow works. You don't necessarily need to follow all of it, but the broad strokes are good to know - that way if you want to deviate from that for simplicity in a particular situation, you'll know why you're doing it and what compromises you're making (and whether they matter in this case).
For your data update... there's two ways you can go about it. One is to make the ViewModel (gonna say VM for brevity) update the data layer, and then have the data layer push new data to the VM, which the VM displays in its LiveData. This is the kind of thing you do when you're using observable queries with a database, where updates to a table push new data to the observer.
All the VM has to worry about is pushing data to the data layer (e.g. calling a delete item function). When the data changes, it's pushed to the observing VM, which just displays the data as usual (e.g. setting it on a LiveData), which causes the UI to get an update and display the new state... So instead of the VM getting the delete event, and having to worry about updating its own internal state, it just passes the delete request to the data layer. Then the new state arrives later, and the VM just uses that - a new list, whatever. The data layer tells the view model what to display, in the same way the VM tells the UI what to display.
(It might not be worth the effort writing a thing that updates the SharedPreferences and then tells the VM to display that data, so you could just do all this in the VM as a kind of combination of VM and data layer - but it helps to know why you're doing it, what it's a shortcut for, y'know?)
As for the "lots of LiveData objects" bit, have a look at this section on UI State. Basically, the officially recommended approach is that the the UI's state as a whole is pushed by the VM. So instead of separate LiveData objects for each property (name, age etc) you'd have a single object that contains that data, and a LiveData that pushes that. Whenever something about that data changes, you push a new instance of that data object. (Data classes can help here, with their copy functions that let you change specific values and keep the rest)
So your UI just observes that one state LiveData, and wires it up to the UI components - your TextViews, CheckBoxes etc. The approach they're talking about isn't just data - it's also UI state, which may not be what you're storing in your actual data layer (e.g. if a particular section is expanded, or if some list items are checked for a potential delete operation). Exactly how much you want to encapsulate in one object is up to you - but pushing your actual data in a single data structure isn't a bad idea!
Sorry that was a bit long, but hopefully it helps a bit

Use observe for a variable that updated inside another observe in Kotlin

I am trying first handle the response from API by using observe. Later after observing the handled variable I want to save it to database.
The variable tokenFromApi is updated inside tokenResponseFromApi's observer. Is it possible to observe tokenFromApi outside the observer of tokenResponseFromApi? When debugged, the code did not enter inside tokenFromApi observer when the app started.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
var tokenResponseFromApi: LiveData<String>? = MutableLiveData<String>()
var tokenFromApi: LiveData<TokenEntity>? = MutableLiveData<TokenEntity>()
tokenResponseFromApi?.observe(viewLifecycleOwner, Observer {
tokenResponseFromApi ->
if (tokenResponseFromApi != null) {
tokenFromApi = viewModel.convertTokenResponseToEntity(tokenResponseFromApi, dh.asDate)
}
})
tokenFromApi?.observe(viewLifecycleOwner, Observer {
tokenFromApi ->
if (tokenFromApi != null) {
viewModel.saveTokenToDB(repo, tokenFromApi)
}
})
}
Your problem is that you're registering the observer on tokenFromApi during setup, and when you get your API response, you're replacing tokenFromApi without registering an observer on it. So if it ever emits a value, you'll never know about it. The only observer you have registered is the one on the discarded tokenFromApi which is never used by anything
Honestly your setup here isn't how you're supposed to use LiveData. Instead of creating a whole new tokenFromApi for each response, you'd just have a single LiveData that things can observe. When there's a new value (like an API token) you set that on the LiveData, and all the observers see it and react to it. Once that's wired up, it's done and it all works.
The way you're doing it right now, you have a data source that needs to be taken apart, replaced with a new one, and then everything reconnected to it - every time there's a new piece of data, if you see what I mean.
Ideally the Fragment is the UI, so it reacts to events (by observing a data source like a LiveData and pushes UI events to the view model (someone clicked this thing, etc). That API fetching and DB storing really belongs in the VM - and you're already half doing that with those functions in the VM you're calling here, right? The LiveDatas belong in the VM because they're a source of data about what's going on inside the VM, and the rest of the app - they expose info the UI needs to react to. Having the LiveData instances in your fragment and trying to wire them up when something happens is part of your problem
Have a look at the App Architecture guide (that's the UI Layer page but it's worth being familiar with the rest), but this is a basic sketch of how I'd do it:
class SomeViewModel ViewModel() {
// private mutable version, public immutable version
private val _tokenFromApi = MutableLiveData<TokenEntity>()
val tokenFromApi: LiveData<TokenEntity> get() = _tokenFromApi
fun callApi() {
// Do your API call here
// Whatever callback/observer function you're using, do this
// with the result:
result?.let { reponse ->
convertTokenResponseToEntity(response, dh.asDate)
}?.let { token ->
saveTokenToDb(repo, token)
_tokenFromApi.setValue(token)
}
}
private fun convertTokenResponseToEntity(response: String, date: Date): TokenEntity? {
// whatever goes on in here
}
private fun saveTokenToDb(repo: Repository, token: TokenEntity) {
// whatever goes on in here too
}
}
so it's basically all contained within the VM - the UI stuff like fragments doesn't need to know anything about API calls, whether something is being stored, how it's being stored. The VM can update one of its exposed LiveData objects when it needs to emit some new data, update some state, or whatever - stuff that's interesting to things outside the VM, not its internal workings. The Fragment just observes whichever one it's interested in, and updates the UI as required.
(I know the callback situation might be more complex than that, like saving to the DB might involve a Flow or something. But the idea is the same - in its callback/result function, push a value to your LiveData as appropriate so observers can receive it. And there's nothing wrong with using LiveData or Flow objects inside the VM, and wiring those up so a new TokenEntity gets pushed to an observer that calls saveTokenToDb, if that kind of pipeline setup makes sense! But keep that stuff private if the outside world doesn't need to know about those intermediate steps

why should we use ViewModel if we can directly access Repository from Fragment in MVVM

I am a beginner and I would like to know that why we use ViewModel in MVVM, when we can directly access Repository and call Repository functions from Fragment. This approach seems easier.
Please see below details why I ask this question.
In case you would like to see the codes, please refer below question in Stackoverflow:
Recycler view shows all data from Room database instead of just showing data of selected ids
I had a problem where I wanted to show details of a selected id in RecyclerView, but when I was using ViewModel the RecyclerView always showed the details of all ids.
Then instead of accessing Repository through ViewModel, I directly accessed the Repository and solved the problem. Now the RecyclerView shows details of only the selected id.
The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.
The Android framework manages the lifecycles of UI controllers, such as activities and fragments. The framework may decide to destroy or re-create a UI controller in response to certain user actions or device events that are completely out of your control.
If the system destroys or re-creates a UI controller, any transient UI-related data you store in them is lost. For example, your app may include a list of users in one of its activities. When the activity is re-created for a configuration change, the new activity has to re-fetch the list of users. For simple data, the activity can use the onSaveInstanceState() method and restore its data from the bundle in onCreate(), but this approach is only suitable for small amounts of data that can be serialized then deserialized, not for potentially large amounts of data like a list of users or bitmaps.
Another problem is that UI controllers frequently need to make asynchronous calls that may take some time to return. The UI controller needs to manage these calls and ensure the system cleans them up after it's destroyed to avoid potential memory leaks. This management requires a lot of maintenance, and in the case where the object is re-created for a configuration change, it's a waste of resources since the object may have to reissue calls it has already made.
It's easier and more efficient to separate out view data ownership from
UI controller logic.
For more info and sample check this
you have to separate your business logic from the UI logic also you can do better exception handling when you use view model and use repository for the data connection.
you have applied wrong logic.
to solve the above problem send data from your activity to view model and view model to the repository and then get data from ViewModel to the activity of that particular id. Hops this will helpful for you.
in Activity
viewmodel1.ProfileFetch(id)
then in ViewModel
fun ProfileFetch(id:String?) {
Coroutines.main {
try {
val response = repository.StudentProfile(id)
response?.let {
if(response.statusResponse!!.statusCode.equals("1")){
Log.v("sssss","ppppppp")
studentProfileResponseListner?.onSuccess(response)
}else{
studentProfileResponseListner?.onFailure(response!!.statusResponse?.message)
}
}
} catch (e: ApiException) {
studentProfileResponseListner?.onFailure(e.message!!)
} catch (e: NoInternetException) {
studentProfileResponseListner?.onFailure(e.message!!)
} catch (e: SocketTimeoutException) {
studentProfileResponseListner?.onFailure("Network is unreachable")
} catch (e: Exception) {
studentProfileResponseListner?.onFailure("Sorry for inconvenience, system has encountered technical glitch")
}
}
}
pass id from ViewModel to the repository
val response = repository.StudentProfile(id)

Android ViewModel with Fragments causing the same data to be populated in different fragments

I recently switched to Android MVVM on one of my Projects, the problem that I am facing is that with my Fragments used with ViewPager and TabLayout the data for each tab must be different based on an id of each tab, however since I am using AndroidViewModel to connect to my data source, the same data is shown in all my tab fargments. I understand that problem is that same ViewModel is shared between all the dynamic fragments[Fragmnet Class being the same].
Is there any way around this? or if I am doing something wrong.
//Code that returns data
private MutableLiveData<List<InventoryProduct>> inventoryProductList;
//we will call this method to get the data
public LiveData<List<InventoryProduct>> getCategoriesList(String cat_id,String store_id) {
//if the list is null
if (inventoryProductList == null) {
inventoryProductList = new MutableLiveData<>();
//we will load it asynchronously from server in this method
loadInventoryProducts(cat_id,store_id);
}
//finally we will return the list
return inventoryProductList;
}
There is nothing wrong with having the same ViewModel for multiple fragments, in fact, it helps in a lot of ways. In your case, I would suggest that keep some identifier in the fragment, which you can pass to the ViewModel's function and accordingly decide what data to provide. This way different fragments would have different data and your data would still be persistent as long as the LifeCycleOwner is alive.
Following from the edited question, you will need to remove the null check, as the same instance of ViewModel is being used, once initialized, inventoryProductList is never null again and hence the subsequent functions are getting the data of the first fragment. As a solution(If you don't want to go the DB way), you can maintain a Map of the LiveData like this
Map<CatId/StoreId,LiveData<List<InventoryProduct>>> dataMap=new HashMap();
Now Instead of null check, you check the map for your CatId/StoryID (based on what key you have already used) and if the Map does not have the value already, go for the API call, otherwise return the value from the map.
Something like this
Say you had used StoreID as the Key
if(!dataMap.containsKey(store_id)){
MutableLiveData<List<InventoryProduct>> inventoryProductList = new MutableLiveData<>();
//we will load it asynchronously from server in this method
loadInventoryProducts(cat_id,store_id);
dataMap.put(store_id,inventoryProductList);
//You need to post the response from the api call in this inventoryProductList
}
return dataMap.get(store_id);
Make sure that once you get the API response for the corresponding cat_id/store_id, you actually post the data to the corresponding LiveData.

Notifying LiveData observers but without passing them any data

I have a case where I have LiveData observer that monitors a condition that indicates if the user is signed in. The observer will only get notified when the user is signed in. I don't need to pass any data to the observer. When the observer gets called, it simply means that the user is signed in:
val observer = Observer<String> { signedIn ->
// The user is signed in. Do something...
}
model.isSignedIn.observe(this, observer)
In my viewmodel I believe I'm suppose to update the observer as follows:
isSignedIn.setValue()
Is this the proper way to update an observer that doesn't require any data sent to it? LiveData is really about notifying observers about data changes. But in my example, I'm using it to notify about an event change. It's a subtle difference and maybe using LiveData for this case is not the best way of doing it.
In that case you can use LiveData, it has no restrictions, especially if you want to be lifecycle aware.
If you want to have more clear API for that case you can use extension function mechanism. And in your case, suggest to use Unit type for live data variable.
typealias NoValueLiveData = MutableLiveData<Unit>
fun NoValueLiveData.setValue() {
this.value = Unit
}

Categories

Resources