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

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

Related

Should Kotlin Flows be used when passing a single object to ViewModel in Android?

On various occasions I've seen people use Kotlin Flows when retrieving a list of objects from a local database, but nowhere have I noticed any warning/error about using them when retrieving single objects (as opposed to a list), or even specification that Flows are to be used only on lists
My question is, will using Flows on single objects cause an error/problem? If not does it hinder performance? In case no, why is it generally not used then?
Sample code of what I mean:
Single object retrieval with Flows:
#Query("SELECT * FROM objects WHERE id = :id")
fun getObjectById(id: Int): Flow<Object>
Retrieving function in ViewModel:
fun objectRetrieval(id: Int) {
// ... More code
getObjectById(id)
.onEach { object: Object ->
// Operations
}
.launchIn(viewModelScope)
}
Thank you for your time!
I think there are multiple misconceptions here, both in the question and in comments/answer.
Flows are indeed related to sequences of values processed asynchronously. In simple words that means: if in the future we plan to receive some data multiple times, flows are just for this purpose. Examples are: user events, incoming messages in an instant messaging application, etc.
If we plan to receive the data only once, we can still use flow, but this is an overkill and it adds unnecessary complication. In these cases it is better to use a suspend function as it is easier to use and it is very clear about the fact the data is received only once.
However, it doesn't really matter, if the data we receive is a single object or a list of objects. As a matter of fact, list is an object as well:
suspend fun foo(): String - receive a single object once.
suspend fun foo(): List<String> - receive a list of objects, all at once.
fun foo(): Flow<String> - receive a single object multiple times.
fun foo(): Flow<List<String>> - receive a list of objects multiple times.
All of above cases make sense and can be used in different scenarios.
Now, going to your specific example, there is another misconception there. Your getObjectById() doesn't receive only a single value, but a sequence of values. By using Flow we say to Room that we want to observe the data for changes. First, it sends you the current data for the object, but if the object ever changes, you receive updated data again.
If you want to only get the current data and do not observe for changes, use a suspend function instead.
Yes you should flow single objects Singe Source of Truth Architecture
val numLightsObserver: Observer<Int> = Observer { i: Int ->
i.let { jx.setTitle(i.toString()) }
}
wordViewModel.numLights.observe(this, numLightsObserver)
Now I have a fully automated object for my UI what will I do with the number of lights. Above I convert it to a string for this demo commit.
here's a link to commit diff I had to modify 4 files to make it work
diff on github where I try it flow a single object

Passing Data Class To Other Composable Screen

i have a data class
data class Holder(uri: String, title: String, desc: String, source: String, color: String?) that i have from screen a, that i want to pass along to screen b. i have the code set up as follows:
#Composable
fun A(navigateToScreenB: (holder: Holder) -> Unit) {...}
#Composable
fun B(holder: Holder) {...}
in my nav file:
composable(
route = Screen.b.route
) {
B(how to get `Holder` to pass here)
}
composable(
route = Screen.A.route
) {
A(
navigateToScreenB = { it ->
// `it` is my data class `Holder`, but how to pass it to screen b?
navController.navigate(Screen.B.route)
}
)
}
any insight on this?
tl/dr: you shouldn't be passing the object at all, the same way you don't see a website that is example.com/data/{the whole object in the URL}, you'd see example.com/data/{a unique ID used to retrieve the object}
As per this thread, if the object exists only at the UI layer (i.e., it isn't coming from a remote database or any other source of truth), you can just pass the object directly by following the documentation and making your class Parcelable and serializing it to include it as part of your Screen.b.route just like any other argument.
However, you've said that your Holder object actually comes from a remote database. That same thread goes on to discuss exactly this case:
A better way to start the conversation is figuring out how StoreList is going to recreate its list after process death and recreation. Your objects have been wiped from memory entirely, which means that ViewModel needs to be talking to the actual source of truth (i.e., your repository layer - note that doesn't mean "save to disk" or "database", it just means the layer responsible for getting your data)
Once you have a repository layer anyways (since a ViewModel shouldn't be doing network calls or disk access directly), that's also the layer that can do in memory caching
Keep in mind that every list already has a way to uniquely define each item by the index in the list
So at that point you already have everything you need - a repository both destinations can talk to that is the single source of truth for Store objects and an index into the list that you can pass to the details screen to define you element
And if you start persisting those objects into a database, the rest of the layers don't have to care
(of course, you probably will add a unique ID to each element if you do add it to a database at some point, which then makes the index based approach unnecessary)
So how you should actually be doing this is:
Create a data layer that is responsible for loading your data. No other layer should know that your data is actually from a remote database. Okhttp caching is a simple way to add caching to prevent redownloading data in a way that is transparent to the other layers.
Both Screen A and Screen B would talk to the same data layer, generally, as per the UI Layer documentation, would be by using a state holder such as a ViewModel that is responsible for loading data from the data layer.
2a) Screen A would request the entire list of Holder objects from the data layer, which would load the entire list from the remote database.
2b) Screen B would request just a single Holder object, either by using the index in the list or a unique key if one is available (it isn't clear from your Holder class if a unique key exists)
Instead of passing the entire Holder as part of Screen B's route argument, you'd pass only that unique key, which could simply be the index in the list or something more complicated if your remote database has such a unique key (note that a Uri would need to be encoded via Uri.encode if included in a route)
This approach follows the approach of a single source of truth. If you later change your data layer to store data locally, none of the rest of your layers need to change. If Screen B gains the ability to edit your Holder objects, then Screen A (assuming your data layer uses a Flow or similar observable structure) would automatically update, without needing to pass data back or anything complicated like that.
It also means that cases which are generally hard to handle, such as configuration changes (i.e., rotating your device) or process death and recreation (which can happen at any screen, meaning you can't rely on Screen A to have loaded your data into memory) are also handled without any additional work.

Changing Data Class From Live Data

I have a BaseViewModel that basically has the function to get the user data like so:
abstract class BaseViewModel(
private val repository: BaseRepository
) : ViewModel() {
private var _userResponse: MutableLiveData<Resource<UserResponse>> = MutableLiveData()
val userResponse: LiveData<Resource<UserResponse>> get() = _userResponse
fun getUserData() = viewModelScope.launch {
_userResponse.value = Resource.Loading
_userResponse.value = repository.getLoggedInUserData()
}
}
In my Fragment, I access this data by just calling viewModel.getUserData(). This works. However, I'd like to now be able to edit the data. For example, the data class of UserResponse looks like this:
data class UserResponse(
var id: Int,
var username: String,
var email: String
)
In other fragments, I'd like to edit username and email for example. How do I do access the UserResponse object and edit it? Is this a good way of doing things? The getUserData should be accessed everywhere and that is why I'm including it in the abstract BaseViewModel. Whenever the UserResponse is null, I do the following check:
if (viewModel.userResponse.value == null) {
viewModel.getUserData()
}
If you want to be able to edit the data in userResponse, really what you're talking about is changing the value it holds, right? The best way to do that is through the ViewModel itself:
abstract class BaseViewModel(
private val repository: BaseRepository
) : ViewModel() {
private var _userResponse: MutableLiveData<Resource<UserResponse>> = MutableLiveData()
val userResponse: LiveData<Resource<UserResponse>> get() = _userResponse
fun setUserResponse(response: UserResponse) {
_userResponse.value = response
}
...
}
This has a few advantages - first, the view model is responsible for holding and managing the data, and provides an interface for reading, observing, and updating it. Rather than having lots of places where the data is manipulated, those places just call this one function instead. That makes it a lot easier to change things later, if you need to - the code that calls the function might not need to change at all!
This also means that you can expand the update logic more easily, since it's all centralised in the VM. Need to write the new value to a SavedStateHandle, so it's not lost if the app goes to the background? Just throw that in the update function. Maybe persist it to a database? Throw that in. None of the callers need to know what's happening in there
The other advantage is you're actually setting a new value on the LiveData, which means your update behaviour is consistent and predictable. If the user response changes (either a whole new one, or a change to the current one) then everything observeing that LiveData sees the update, and can decide what to do with it. It's less brittle than this idea that one change to the current response is "new" and another change is "an update" and observers will only care about one of those and don't need to be notified of the other. Consistency in how changes are handled will avoid bugs being introduced later, and just make it easier to reason about what's going on
There's nothing stopping you from updating the properties of the object held in userResponse, just like there's nothing stopping you from holding a List in a LiveData, and adding elements to that list. Everything with a reference to that object will see the new data, but only if they look at it. The point of LiveData and the observer pattern is to push updates to observers, so they can react to changes (like, say, updating text displayed in a UI). If you change one of the vars in that data class, how are you going to make sure everything that needs to see those changes definitely sees them? How can you ensure that will always happen, as the app gets developed, possibly by other people? The observer pattern is about simplifying that logic - update happens, observers are notified, the end
If you are going to do things this way, then I'd still recommend putting an update function in your VM, and let that update the vars. You get the same benefits - centralising the logic, enabling things like persistence if it ever becomes necessary, etc. It could be as simple as
fun setUserResponse(response: UserResponse) {
_userResponse.value?.run {
id = response.id
username = response.username
email = response.email
}
}
and if you do decide to go with the full observer pattern for all changes later, everything is already calling the function the right way, no need for changes there. Or you could just make separate updateEmail(email: String) etc functions, whatever you want to do. But putting all that logic in the VM is a good idea, it's kinda what it's there for
Oh and you access that object through userResponse.value if you want to poke at it - but like I said, better to do that inside a function in the VM, keep that implementation detail, null-safety etc in one place, so callers don't need to mess with it
The ideal way to update userResponse you should change/edit _userResponse so that your userResponse we'll give you the updated data.
it should be something like this
_userResponse.value = Resource<UserResponse>()

Android architecture LiveData and Repositories

I am converting my application to room database and try to follow the google architecture best practices based on "Room with a View".
I am having trouble to understand the repository in terms of clean architecture.
The Words database example contains only one table and one view using it, making it a simple HelloWorld example. But lets start with that.
There is a view which displays a list of words. Thus all words need to be read from the database and displayed.
So we have a MainActivity and a Database to connect.
Entity Word
WordDao to access DB
WordViewModel: To separate the activity lifecycle from the data lifecycle a ViewModel is used.
WordRepository: Since the data maybe kept in a database or the cloud or whatever the repository is introduced to handle decision, where data comes from.
Activity with the View
It would be nice if the view is updated when the data changes, so LiveData is used.
This in turn means, the repository is providing the LiveData for the full table:
// LiveData gives us updated words when they change.
val allWords: LiveData<List<Word>>
This is all fine for a single view.
Now to my questions on expanding this concept.
Let us assume, the word table has two columns "word" and "last_updated" as time string.
For easier comparison the time string needs to be converted to milliseconds, so I have a function.
Question: Where to put the fun queryMaxServerDateMS() to get the max(last_updated)?
/**
* #return Highest server date in table in milliseconds or 1 on empty/error.
*/
fun queryMaxServerDateMS(): Long {
val maxDateTime = wordDao.queryMaxServerDate()
var timeMS: Long = 0
if (maxDateTime != null) {
timeMS = parseDateToMillisOrZero_UTC(maxDateTime)
}
return if (timeMS <= 0) 1 else timeMS
}
For me it would be natural to put this into the WordRepository.
Second requirement: Background job to update the word list in the database.
Suppose I now want a Background Job scheduled on a regular basis which checks the server, if new entries were made and downloads them to the database. The app may not be open.
This question just relays to the question of the above queryMaxServerDateMS.
The job will basically check first, if a new entry was made by asking the server if an entry exists which is newer then the max known entry.
So I would need to get a new class WordRepository, do my query, get max last_update and ask the server.
BUT: I do not need the LiveData in the background job and when val repositoy = WordRepository the full table is read, which is needless and time-, memory and batteryconsuming.
I also can think of a number of different fragments that would require some data of the word table, but never the full data, think of a product detail screen which lists one product.
So I can move it out to another Repository or DbHelper however you want to call it.
But in the end I wonder, if I use LiveData, which requires the View, ViewModel and Repository to be closely coupled together:
Question: Do I need a repository for every activity/fragment instead of having a repository for every table which would be much more logical?
Yes, with your current architecture you should put it in the Repository.
No, you don't need a repository for every activity/fragment. Preferably, 1 repository should be created for 1 entity. You can have a UseCase for every ViewModel.
In Clean architecture there's a concept of UseCase / Interactor, that can contain business logic, and in Android it can act as an additional layer between ViewModel and Repository, you can create some UseCase class for your function queryMaxServerDateMS(), put it there and call it from any ViewModel you need.
Also you can get your LiveData value synchronously, by calling getValue().
You do not need repository for each activity or fragment. To answer your question about getting max server time - when you load words from db you pretty much have access to entire table. That means you can either do that computation yourself to decide which is the latest word that's added or you can delegate that work to room by adding another query in dao and access it in your repo. I'd prefer latter just for the simplicity of it.
To answer your question about using repo across different activities or fragment - room caches your computations so that they are available for use across different users of your repo (and eventually dao). This means if you have already computed the max server time in one activity and used it there, other lifecycle owners can use that computed result as far as the table has not been altered (there might be other conditions as well)
To summarize you're right about having repository for tables as opposed to activities or fragments

Binding on a not (yet) initialized livedata element

maybe the headline does not fit the question really well so I'll explain it.
In my app I send a request to a server to get gerneral user information. If I receive on I want to save it into the local room db. Now we come to the problem. I want to bind this one user to the view directly out of the db. But I can not bind a element, which maybe does not exists, because the request is in progress.
(My bad solution): Creating another livedate element which holds a boolean. I create a observer in the Activity and add the observer after the boolean observes a "true". With this solution I can not use "Data Binding" in the xml layout.
Does anyone have an idea? (If you need further information just ask - I know it is a really abstract question without any code)
As Sanlok Lee mentioned:
If you reassign user then it becomes a completely different instance and the observer will not listen to the new LiveData. Instead you can do val user: MediatorLiveData<User> and later you can call user.addSource(dao.getUserById(1), ...)
OR:
Just load the user (also if you know that there is no one in the DB) from the DB. You can do that in you UserRepository for example.
val user: LiveData<User> = userDao.getUser()
The Livedata will get notified when there is a valid user inserted.

Categories

Resources