Update Object type LiveData with Databinding - android

I want to update views via databinding with livedata. Lets have a look at the scenario.
Data Class
data class Movie(var name: String = "", var createdAt: String = "")
ViewModel
class MyViewModel: ViewModel(){
var pageTitle: MutableLiveData<String>()
var movie: MutableLiveData<Movie>()
fun changeTitleAndMovieName()
pageTitle.value = "Title Changed"
movie.value.name = "Movie Name Changed"
}
}
XML
<layout>
...
<TextView
...
android:text="#{`Title: `+viewmodel.pageTitle `Name: `+viewmodel.movie.name}"/>
<Button
...
android:onClick="#{() -> viewmodel.changeTitleAndMovieName()}"/>
</layout>
What I want to do?
When the button is pressed, the title and the name of movie should change and reflect to the view.
What is happening now?
Only page title is changing because of String type LiveData.
Movie name is NOT being reflected in the view because of Movie type LiveData and I am changing the property of Movie type LiveData's property.
Is there any way to update Movie type LiveData to the view when any property is changed of the Movie.
I dont want to re-assign the object to the livedata e.g. viewmodel.movie.value = Movie(...)

I have got the answer of my question. A hint from Here
The reference link's answer is a bit long change todo. I have got a very simple solution.
Here's what I did:
Just inherit you Data Class with BaseObservable and just call the method notifyChange() after your Object's property change from anywhere.
i.e.
Data Class
data class Movie(var name: String = "", var createdAt: String = "") : BaseObservable()
ViewModel
class MyViewModel: ViewModel(){
var pageTitle: MutableLiveData<String>()
var movie: MutableLiveData<Movie>()
fun changeTitleAndMovieName()
pageTitle.value = "Title Changed"
movie.value.name = "Movie Name Changed"
//here is the megic
movie.value.notifyChange()
}
}

Related

Android, how to observe LiveData from within the same ViewModel

I'm a complete novice when it comes to LiveData and MVVM architecture. I'm trying to figure out how to observe a LiveData<List> in the ViewModel to update another variable depending on if it is empty or not.
I'm getting the LiveData from my Room database with this:
class MealsViewModel #Inject constructor(
private val mealDao : MealDao
) : ViewModel() {
...
private val currentDay: MutableLiveData<Date> = MutableLiveData(Date())
val meals = Transformations.switchMap(currentDay){ date -> mealDao.getMeals(date).asLiveData() }
I would like for another variable in the ViewModel, private val empty: Boolean, to update anytime the list is returned empty (null). This will be used in updating an ImageView in the Fragment from Visible.GONE to Visible.VISIBLE.
How do I check if val meals is empty synchronously?
I've read around and saw some people said to useobserveForever, but the architecture guide explicitly advises against any observers in ViewModels.
I could probably observe the LiveData in the Fragment, but that would require business logic in the Fragment, ie:
viewModel.meals.observe(viewLifecycleOwner) {
if meals.value.isEmpty() imageView.visibility = View.VISIBLE else imageView.visibility = View.GONE
}
And I'd like to keep the Fragment as 'dumb' as possible, so I'd prefer to have that logic in the ViewModel. Is that possible?
You can check live data meal to see if it's empty or null and then trigger with your live data empty like this:
In the viewmodel, you create a livedata isEmptyMeals. This live data variable will always trigger when meals value change and will check if your meals value are empty or null.
MealsViewModel.kt
class MealsViewModel #Inject constructor(
private val mealDao : MealDao
) : ViewModel() {
...
private val currentDay: MutableLiveData<Date> = MutableLiveData(Date())
val meals = Transformations.switchMap(currentDay){ date -> mealDao.getMeals(date).asLiveData() }
val isEmptyMeals = meals.map {
it.isNullOrEmpty()
}
}
And in the fragment, you will listen to observe the livedata isEmptyMeals and perform the logic to hide or show the image view you want.
Fragment.kt
viewModel.isEmptyMeals.observe(viewLifecycleOwner) {
imageView.visibility = if (it) View.VISIBLE else View.GONE
}
I don't know exactly how your code is set up but you can do something like below
Add variable to ViewModel
val empty = MutableLiveData<Boolean>()
In meals observer
viewModel.meals.observe(viewLifecycleOwner) {
viewModel.empty,postValue(meals.value.isEmpty())
Then observe from empty
Using MediatorLiveData
In your ViewModel class, create
val empty = MediatorLiveData<Boolean>()
Then
empty.addSource(meals) {
empty.value = it.isEmpty()
}

How to get json data from url to Text Composable?

How can I fetch json from url and add it's data to a Text Composable in Jetpack Compose
Here is json file
https://jsonplaceholder.typicode.com/posts
#Composable
fun Api(){
val queue = Volley.newRequestQueue(LocalContext.current)
val url = "https://jsonplaceholder.typicode.com/posts"
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET, url,null,
{ response ->
val title = response.getString("title")
print(title)
},
{ error ->
print(error.localizedMessage)
})
queue.add(jsonObjectRequest)
}
Just get the data however you want to inside a viewmodel. Then, store it in a variable, like var data by mutableStateOf("")
Then access this variable through the viewmodel from your text Composable. Updating this variable like a normal string will trigger recompositions
EDIT BASED ON THE COMMENT BELOW:-
Although it is not necessary to store it in a viewmodel, it is the recommended best practice. You can also store the state inside your normal activity class or even the Composable using remember(not recommended for important state storage)
However, by viewmodel, I just meant,
class mViewModel: ViewModel(){
var data by mutableStateOf("")
private set //only viewmodel can modify values
fun onLoadData(){
data = //json extraction logic
}
fun onDataChange(newData: String){
data = newData
}
}
Then, in your activity,
class mActiviry: AppCompatActivity(){
val vm by viewmodels<mViewModel>() //See docs for better approaches of initialisation
//...
setContent {
Text(vm.data)
}
}
Done
Edit:-
Alternately, ditch the onDataLoad()
class mViewModel: ViewModel(){
var data by mutableStateOf("")
private set //only viewmodel can modify values
init{
data = // code from the "Api" method in your question
}
fun onDataChange(newData: String){
data = newData
}
}

Kotlin apply function with data class

I have a data class like this -
data class User(val id: String, val name: String)
I want to use apply scope function to modify the values and return the modified object.
User("1", "Name").apply {
this.id = "2" // Gives me error since val cannot be assigned
}
I do not want to make my data class immutable and also don't want to use copy function. How do I fix this?
You can use copy method with data classes:
val user1 = User("1", "Name")
val user2 = user1.copy(id = "2")
change like this data class User(var id: String, var name: String)

Kotlin: How to change values of MutableLiveData in ViewModel without using getters and setters

Here is my viewmodel:
class MyProfileEditSharedViewModel : ViewModel() {
val question = MutableLiveData<String>()
val answer = MutableLiveData<String>()
fun setQuestion (q: String) {
question.value = q
}
fun setAnswer (a: String) {
answer.value = a
}
}
I set the data using setQuestion and setAnswer like this:
viewModel.setQuestion(currentUserInList.question)
viewModel.setAnswer(currentUserInList.answer)
I try to get question and answer from the ViewModel like this:
val qnaQuestionData = communicationViewModel.question as String
val qnaAnswerData = communicationViewModel.answer as String
Compiler says I cannot cast MutableLiveData to string.
Should I make a separate getter like my setter? I heard that you don't need to use getters and setters in kotlin, is there anyway to edit val question and val answer in my viewmodel without using getters and setters?
Thank you!!
You can't cast it to String because the type of object is MutableLiveData, but you can access the value with .value property
val qnaQuestionData = communicationViewModel.question.value
val qnaAnswerData = communicationViewModel.answer.value
in this case, may facing errors about MutableLiveData initialization.
another way is observing the LiveData for changes:
communicationViewModel.question.observe(this, Observer{ data->
...
})
Or if you have not accessed to any lifecycle owner
communicationViewModel.question.observeForever(Observer{ data->
...
})
but please remember to remove the observer through removeObserver method
for setting the values it's better to use properties directly or binding way
communicationViewModel.question.postValue("some new value")
Or
communicationViewModel.question.value = "some new value"
Suggestion for MutableLiveData properties:
val question: MutableLiveData<String> by lazy { MutableLiveData<String>() }
val answer: MutableLiveData<String> by lazy { MutableLiveData<String>() }
https://developer.android.com/reference/android/arch/lifecycle/LiveData
Create some sort of getter method in your ViewModel
fun getQuestion(): LiveData<String> {
return question //this works because MutableLiveData is a subclass of LiveData
}
Then, you can observe the value in whatever class you care about the value. ie:
communicationsViewModel.getQuestion().observe(this, Observer {
//do something with the value which is 'it'. Maybe qnaQuestionData = it
}
Note if you're trying to observe the value from a fragment or something, you will have to change the parameter this, to viewLifecycleOwner

Android LiveData Transformation: Changing LiveData object value

I have one LiveData object that holds a list of Users and I am trying to transfer over the data to another LiveData object to be used elsewhere.
I am using MVVM with Room so I get LiveData from the database and on the ViewModel, I am trying to convert the User object in the LiveData to a Person object to show in the UI.
So I have one variable that is LiveData<List<User>>
class User(var firstName: String, var lastName: String, var age: Integer)
and I am trying to convert it to LiveData<List<Person>> (as an example)
class Person() {
lateinit var firstName: String
lateinit var age: Integer
}
and the way I am trying to change them is by using LiveData Transformations.map
ViewModel:
val list2: LiveData<List<User>> = repo.getAll()
var liveList: LiveData<ArrayList<Person>> = MutableLiveData()
liveList = Transformations.map(list2) { list ->
val newList: ArrayList<Person> = ArrayList()
list?.forEach {
val temp = Person()
temp.firstName = it.firstName
temp.age = it.age
newList.add(temp)
}
return#map newList
}
but when I run it, it crashes or doesn't update the UI.
Thanks!
The main problem with your code is that it uses var in var liveList: LiveData instead of using val.
You should declare the liveList variable like this:
val liveList = Transformations.map(list2) { list ->
...
}
Why?
Generally, a LiveData variable should always be declared with val. The reason is that the purpose of LiveData is to allow us to observe the up-to-date value held by the LiveData. We do it by code like this:
liveList.observe(this) { list ->
showList(list)
}
With this code, we ensure that the updated list is always shown. Whenever the list value which is held by liveList changes, the UI is updated as a result. But if the liveList itself also changes, the code will only observe the first LiveData of the liveList variable, and the UI will not be updated correctly.
val liveList = MutableLiveData(repo.getAll().value.orEmpty().map { user ->
Person(user.firstName, user.age)
})
This would be a more compact way, you could pull out the repo.getAll() call into its own variable if you like

Categories

Resources