adding items to MutableList using viewmodel - android

I have a mutable list created in a viewmodel file that saves the data
//questions they cheated on
var cheatedList = mutableListOf<Int>(6)
I linked the viewmodel file with the file that has functions this way
private val quizViewModel : QuizViewModel by lazy {
ViewModelProviders.of(this).get(QuizViewModel::class.java)
}
it is working fine and i checked it. all i need from it is to save a content of Integers into a mutable list... I use this function to do so
showAnswerButton.setOnClickListener {
val answerText = when{
answerIsTrue -> R.string.true_button
else -> R.string.false_button
}
answerTextView.setText(answerText)
//create a function to return the result to MainActivity
setAnswerShownResult(true)
cheaterStatus = true
quizViewModel.cheatedList.add(currentIndex)
println(quizViewModel.cheatedList)
}
the good news is, it saves the index into the list... the bad news is once i move back to another activity, the list is destroyed and nothing is saved in it anymore...
how can i keep the mutablelist saved even if i closed the activity?

If you want the data to be saved even after closing the application and re opening it (Not temporary closing) you can use local storage like Room to save the data...
But if you only want to save the data in a session then as Tenfour04 said you can use a activity and multiple fragment and use a single viewmodel pattern to save.....

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

How to get data from a MutableLiveData

I'm trying to get data from a MutableLiveData; however, it seems like something is wrong with the code, can you guys check for me please?
I can get the object, but I failed to add the object to a mutableList
properties = ArrayList()
propertyViewModel.propertyItemLiveData.observe(
viewLifecycleOwner,
Observer { propertyItems ->
for (property in propertyItems){
var p:Property = Property(property.id,property.address
,property.price,property.phone,property.lat,property.lon)
println(p)// i can display data
properties.add(p)//when i add to properties, the properties still null. Why?
}
}
)
if (properties.isEmpty()){
println("null")
}
The code in the observer will only run when propertyItemLiveData pushes a new value, or if it already has a value when you first observe it. But from the docs:
As soon as an app component is in the STARTED state, it receives the most recent value from the LiveData objects it’s observing. This only occurs if the LiveData object to be observed has been set.
So you won't actually get a value until your Activity or Fragment hits the onStart() callback, meaning your observer code won't run until then. If the code you posted is running earlier than that (say in onCreate), then what you're doing is:
creating an empty list
adding an observer that will add stuff to that list (but it won't run until later)
checking if the list is still empty (it definitely is)
Because of the observer pattern, where your code reacts to new data/events being pushed to it, whatever you need to do with that populated list should be part of the observer code. It should react to the new value and take action - update a list view, alert the user, start an API call, whatever
propertyViewModel.propertyItemLiveData.observe(viewLifecycleOwner) { propertyItems ->
// handle the propertyItems, add them to your list etc
// then do whatever needs to happen with the list, e.g. display it
updateDisplay(propertyList)
}
btw if Property is a data class and you're just copying all its data, you can add to your list like this:
properties.addAll(propertyItems.map { it.copy() })
// or propertyItems.map(Property::copy)
hello first of all in kotlin in general you have to use mutableList and the check of empty or any other instruction should inside the call back like this :
properties = mutableListOf<YourClass>()
propertyViewModel.propertyItemLiveData.observe(
viewLifecycleOwner,
Observer { propertyItems ->
for (property in propertyItems){
var p:Property = Property(property.id,property.address
,property.price,property.phone,property.lat,property.lon)
println(p)// i can display data
properties.add(p)//when i add to properties, the properties
}
if (properties.isEmpty()){
println("null")
}
}
)

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

How to lazily save ViewModel's SavedStateHandle?

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

Retrieve single record with Room to populate an Edit dialog (activity or fragment)

I have a database table which stores some records. I have been able to correctly populate a RecyclerView in a Fragment, following tutorials like this one and similar ones found via search engine.
What I want to do next is to tie an "Edit record {id}" fragment that is tied to the RecyclerView. In other words, if I click on an item in the Recycler view, another fragment(or activity) should open, load the data for record[id] from the database and then allow me to save and update the record if needed.
The point where I am stuck is retrieving the single record from the database, because I systematically end up with either (1) calling the query inside the main thread, which Room prevents me from doing, or (2) getting some random null pointer.
I have seen solutions even here on stackoverflow, but I can't make sense on how to integrate them in my case.
What I can't make sense of is how to make the async call (whether with threads/coroutines), store the result in a variable, and use it to populate the fields in the Edit fragment.
Internet search have been very disappointing, for all I find are (duplicate) tutorials that are either incomplete, irrelevant or obsolete.
Good pointers are welcome. I would prefer not to use third party libraries to do this, unless someone can explain to me the advantages in doing so.
Sorry for the long post: I haven't added code because there would be too many pieces to show and you would probably know anyway. I will answer any questions however to help out.
Also, I am new to Kotlin/Android, and I am trying to tame this beast :-)
Its hard to say anything specific without any code, but the correct way to do it would be
Retrieve all records from Room
Load them in your recycler view, so recycler adapter will have a list of all your records
setup on click listener in your recycler adapter to open the next activity or fragment
pass the primary key (as in room) of clicked item to the next activity or fragment
In your next activity retrieve a record from room using the primary key
bind the retrieved record to UI
If your recycler view and adapter are correctly setup then you should have following in your adapter
override fun onBindViewHolder(holder: YourViewHolder, position: Int) {
// dataList contains all your records as retrieved from room
// and loaded in your recycler view
setListeners(dataList[position], holder)
holder.bind(dataList[position])
}
private fun setListeners(selectedRecord: YourRecordTypeInRoom, viewHolder: YourViewHolder){
viewHolder.itemView.setOnClickListener {
var intent = Intent(viewHolder.itemView.context, NextActivity::class.java)
// pass primary key to next activity
intent.putExtra("primaryKey", selectedRecord.primaryKey)
viewHolder.itemView.context.startActivity(intent)
}
}
Now to retrieve your single record you should have something as follows in your dao
#Query("Select * FROM your_table where primaryKey = :primaryKey")
fun findByPrimaryKey(primaryKey: PrimaryKeyType): YourRecordType
Edit:
You can also modify the return type of above function to be a LiveData object, which will allow you to observe it in your activity in an async manner. with live data your code would look some thing as follows.
In Dao
#Query("Select * FROM your_table where primaryKey = :primaryKey")
fun findByPrimaryKey(primaryKey: PrimaryKeyType): LiveData<YourRecordType>
In your view model
fun getRecordByPrimaryKey(primaryKey: PrimaryKeyType) = yourDao.findByPrimaryKey(primaryKey)
and in your activity or fragment
viewModel.getRecordByPrimaryKey(primaryKey).observe(this, Observer{
// Bind your record on UI
})
1) Return fun someFunction():LiveData<Model> in Room class, (you should be able to call it from Main thread). After getting value once, you can stop observing, since you want only single value
2) You can use Kotlin Coroutines, this way you return suspend fun someFunction():Model. You can only call this function from another Coroutine, so it will be something like:
class ViewModel{
fun normalFunction(){
viewModelScope.launch{
val result = room.someFunction()
// tell View that you have result (View observes result using LiveData)
}
}
}

Categories

Resources