I was trying to see when binding.invalidateAll() is needed to refresh the UI when the data changes.
I have 2 examples where I change the data, in one of them the UI data changes automatically and in the other, I need to use invalidateAll() to see the changes
First of all, below is the XML of the TextView I'm testing on:
...
<TextView
android:id="#+id/name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#{mainActivity.myName.name}" />
First Example
In the first example, if I change the data inside onCreate() or onResume() the data changes directly without the need for invalidateAll(), please find the code below:
private lateinit var binding: ActivityMainBinding
var myName = MyName(name = "Test")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.mainActivity = this;
myName.name = "Data is visible directly"
}
Here, once the UI is drawn to the user, I see the TextView's text directly set as "Data is visible directly" without the need to call binding.invalidateAll()
Second Example
In the second example, I only change the text when a button is clicked as in the below code:
...
fun onButtonClickedChangeData(view:View){
myName.name = "Data is visible only after invalidateAll is called"
binding.invalidateAll() // without it the text won't change
}
In the second example, I have to use binding.invalidateAll() for the text of the TextView to change, if I don't use invalidateAll() the text will not change.
My First Speculation (which seems to be wrong)
I first thought maybe binding.invalidateAll() is needed when we change the data while the activity is already running and the UI is already visible to the user, and that invalidateAll() won't be needed if the data changes before the UI is drawn yet to the user (giving that the UI is only visible to the user in onResumue() according to my knowledge).
But when I tried to update the data inside onResumue() the data changed directly as well without the need for binding.invalidateAll()
Related
I'm creating an app that, among other things, enables the user to add a Consumer, and then remove him later. The consumers are shown in cards with a remove button in the end.
Adding a consumer works fine. However, when I try to remove a consumer, the one removed in the app screen is always the last one. I know this is not a logic implementation mistake, because I stopped the Debugger right before the items() call, and in any recomposition the list holding the consumers has the correct consumer removed! The following image shows the result after clicking the Remove button from the "B" card (the card removed is "C"!):
Look what the debugger shows right before the recomposition takes place:
The relevant code is below.
The ViewModel and Model (relevant part) definitions:
class ConsumidoresViewModel : ViewModel() {
var lista = mutableStateListOf<Consumidor>()
fun add(consumidor: Consumidor){
lista += consumidor
}
fun remove(consumidor: Consumidor){
lista.remove(consumidor)
}
}
data class Consumidor(var nome: String)
...
The main composable, called directly from .onCreate():
fun UsersView() {
var consumidores: ConsumidoresViewModel = viewModel()
...
LazyColumn() {
items(items = consumidores.lista) { consumidor ->
CardNome(consumidor, consumidores)
}
}
The fucntion call of the Remove button:
IconButton(onClick = { consumidorViewModel.remove(consumidor) }) { ... }
I can't figure out what I'm doing wrong. I'm fairily new with Android Programming / Compose, but I have been programming for decades (not professionaly). Can someone point me to a direction? It probably has something to do with my Sates / View Model implementation, but I can't find out what, as the SnapshotStateList on the debugger clearly shows "A" and "C" cards present, and "B" gone!
Based on the official docs.
By default, each item's state is keyed against the position of the item in the list or grid. However, this can cause issues if the data set changes, since items which change position effectively lose any remembered state. If you imagine the scenario of LazyRow within a LazyColumn, if the row changes item position, the user would then lose their scroll position within the row.
So it's usually a good set up when your data class has a unique property like an id if you plan to manipulate a collection of it (like your removal operation), you can then use it as a key = {...} for the LazyColumn so it knows not to use the index as a unique identifier for its item elements, and that could be the reason why your'e having a wrong display of items after removing an element from the list.
LazyColumn() {
items(items = consumidorList, key = { it.id }) { consumidorItem ->
...
}
}
Update:
Linking my another answer for a movableContentOf{...} sample.
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>()
In my code I make use of the following Views in XML:
val googleButton: Button = findViewById<View>(R.id.google_login) as Button
val loginWithEmailText: TextView = findViewById(R.id.login_with_email_text)
val emailLoginButton: Button = findViewById(R.id.email_login_button)
val createAccountButton: Button = findViewById(R.id.email_create_account_button)
This code is extracted from a function inside my Kotlin class. Whenever I have to access these views, I need to write this code all over again.
Is there any way that I can access them from only one place in my class code? I tried putting them outside but the app won't start.
Thank you
You need to define these fields as a part of your class and initialize them once you set the layout resource for your Activity/Fragment. If you put these lines 1:1 in the class body, the initialization will fail, since the layout has not been inflated yet.
Please get familiar with the concept of lifecycle, so that you can understand how to approach View related topics: https://developer.android.com/guide/components/activities/activity-lifecycle
Please check out this snippet for a sample code:
class MyActivity: Activity() {
lateinit var textView: TextView
lateinit var button: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
// initialize your views here
textView = findViewById(R.id.text_view_id)
button = findViewById(R.id.button_id)
}
fun someOtherFunction(){
// you can reference your views here like normal properties
button.setOnClickListener { v -> callAnotherFunction() }
// ...
}
}
Since you are on Android, you might be interested in using Kotlin synthetic properties for referencing views without the whole boilerplate of finding them: https://antonioleiva.com/kotlin-android-extensions/. It's no longer a recommended practice to make use of it, but it's handy in some cases anyway.
I have a recycler view with fixed number widgets vertically in a specific order. Some of the widgets also contain tabular data hence I've considered using nested recycler view also within it.
Every widget makes http call asynchronously from the ViewModel and binds the data to the epoxyController as I mentioned below.
As requestModelBuild() being called for every widget as they receive the data through the public setters for example priceViewData, packageData and etc from where requestModelBuild() is called. So in this instance every widget bind happens regardless of every time when data is received for any of the widgets.
This seems to be expensive also, there some analytics gets fired as we needed for every bind.
So, here the analytics call for the widget is multiplied.
Please suggest if this can be handled through the epoxy without handling manually.
class ProductDetailsEpoxyController(val view: View?,
private val name: String?,
private val context: Context?) :
AsyncEpoxyController() {
private val args = bundleOf("name" to name)
var priceViewData: IndicativePriceViewData? = emptyPriceViewData()
set(value) {
field = value
requestModelBuild()
}
var packageData: PackageViewData? = emptyPackageWidgetViewData()
set(value) {
field = value
requestModelBuild()
}
...
...
override fun buildModels() {
buildPriceViewData()
buildPackageViewData()
....
}
private fun buildPriceViewData(){
priceViewData?.let {
id("price")
priceViewDataModel(it)
}
}
private fun buildPackageViewData(){
packageViewData?.let {
id("package")
packageViewDataModel(it)
}
}
...
...
}
From Epoxy's Wiki:
Adapter and diffing details
Once models are built, Epoxy sets the new models on the backing adapter and runs a diffing algorithm to compute changes against the previous model list. Any item changes are notified to the RecyclerView so that views can be removed, inserted, moved, or updated as necessary.
So basicallly, this ensures not all models will be updated.
The issue that you're facing is possibly related to:
Using DataBinding
Your classes are not implemented equals and hashCode the way you want.
The problem with using Objects in DataBinding is that, every time the object is updated, all fields that depend on the object are also updated, even if not all changed.
If your classes are normal classes and not data classes or you expect a different behavior when executing priceData1 == priceData2 (for example, only comparing the data's id), you should override this methods so Epoxy detect changes correctly. Also, you can use DoNotHash option for EpoxyAttribute so the class is not added to the model's hashCode function. More info
I'm new to android development so this may be simple but I just cant see what is wrong.
I have a data class (which is actually a room entity if that matters) , for example :
#Entity(...)
data class MyDataClass (
...
var dataType:Int=0,
...
)
In viewmodel I have
val data = MutableLiveData<MyDataClass>()
In the viewmodel init block this is initialised
In my activity layout xml I have various views to allow editing of the data class
There is a spinner in the layout which edits the dataType :
<Spinner
...
android:selectedItemPosition="#={viewmodel.data.dataType}"
/>
So far it works.
I then have a view in the layout
<LinearLayout
...
app:hideIfZero="#{viewmodel.data.dataType}"
/>
where the hideIfZero binding adapter is
#BindingAdapter("app:hideIfZero")
fun hideIfZero(view: View, number:Int) {
view.visibility = if (number == 0) View.GONE else View.VISIBLE
}
When I run the app, I can edit all of my fields including the dataType via the spinner. However when I change the spinner value the visibility of the LinearLayout is not changed.
What am I missing ?
Do I need to somehow tell the activity to refresh the complete layout. Any examples I've found seem to imply this should happen automatically.