Photo by Rubaitul Azad on Unsplash
Consider a scenario where we have to implement a operation on a recycler view item based on the current state of that item ie. our logic interference must be confined to the state of a single without interfering with the state of other recycler items, and those state changes must survive the rebuilding and removal of the view by recycler view on scroll operations.
Sample App
Lets consider a inventory management app which is used to record to info about the stuff entering the warehouse.
We want to provide a page were user can provide ’N’ different products their quantity, weight, and dimension in order to define the amount of space it going to require. what we also want is validations on every item so that in the end we don’t end up with some weird looking product that has no weight or no quantity etc.
We also want to achieve these validations in real time we don’t want to notify user at end of the form that he missed something in middle. end product would look somewhat like this.
one feature of this app is we can click on any textinput related to any item and our validations are working in real time
Different approached we could take
- ViewModels — Maybe we could some how bind different instances of a same view model to a recycler view item in its viewholder using ViewModelProviders and then observe them or bind these instances to our xml. but in this approach we would need to handle sate of these view-model initiate them and destroy them as user scroll. though this can be implemented but it runs a high risk of ui crashes at run time, maybe a view is initiated but there’s a slight delay in viewmodel initialization creating a “NullPointerException” or ending up with a scenario where a same viewmodel instance is tied to two different view items
- TextWatcher Callbacks — we can also used text watchers to apply same behaviour but sometime they can make our code very complex business — logic wise specially when there are 4–5 watchers involved and they have to interact with each other. they work but sometimes its feel like applying to for loops to a dynamic programming problem
- BaseObservables — we can also apply base observables and move some our logic to the item model itself, then this model can be binded with view itself just like any other model. we would be discussing about this approach in this blog in order to achieve “on-the-fly ui updates to our recycler view items”.
Why not a simple data model class or something
Let’s consider a simple kotlin data class for a product which looks someting like this:
data class Product( | |
var height : String? = null, | |
var length : String? = null, | |
var width : String? = null, | |
var weight : String? = null, | |
var quantity : Int? = null, | |
) |
this data class if perfect, but not for our case because if we create a dependence of weight on quantity, change in quantity will not automatically update our wight, cause its not a live data. hence we cannot bind this product to view like this
<com.google.android.material.textfield.TextInputEditText | |
android:text="@={product.weight}" /> | |
changing quantity will not change weight, or show a error in our textInputLayout if our weight is empty like this.
<com.google.android.material.textfield.TextInputLayout | |
app:error="@{product.weight == null ? 'error' : null }" > | |
<com.google.android.material.textfield.TextInputEditText | |
android:text="@={product.weight}" /> | |
</com.google.android.material.textfield.TextInputLayout> |
in order to that we need a base observable model
BaseObservableModels
Good thing about base observable models is that we can configure them to notify other model variables if one variable is changed. so when they are binded with a view they work similarly as a livedata module inside a viewModel.
class ProductObservable() : BaseObservable(){ | |
constructor(weight : String?) : this() { | |
this.weight = weight | |
} | |
@SerializedName("weight") | |
@get:Bindable | |
var weight: String? = null | |
set(value) { | |
field = value | |
weightError = if (field.isNullOrEmpty() || (field | |
?: "0.00").toDouble() == 0.00 | |
) "Required*" else null | |
notifyPropertyChanged(BR.weightError) | |
notifyPropertyChanged(BR.weight) | |
} | |
@get:Bindable | |
var weightError: String? = null | |
set(value) { | |
field = value | |
notifyPropertyChanged(BR.weightError) | |
} | |
} |
Job Offers
what we have done in above code is that we have passed a fraction of our business logic inside our model. this model can be used to do all tasks that a model do. but as we bind this model to our recycler view item it works as a liveData object and notify our ui of data changes in real time.
This model is achieving this by the use of databinding.BaseObservable
class provided by android .androidx.databinding.BaseObservable
is a class in the Android Data Binding Library that provides a base class for objects that are observed by data binding expressions. It includes methods to notify the data binding system when data changes, so that the expressions can be updated to reflect the new data. in above classes we are updating weightError if current weight value is either zero or null
<data class="ProductInfoBinding"> | |
<variable | |
name="product" | |
type="com.....data.Product" /> | |
</data> | |
<com.google.android.material.textfield.TextInputLayout | |
app:error="@{product.weightError}" > | |
<com.google.android.material.textfield.TextInputEditText | |
android:text="@={product.weight}" /> | |
</com.google.android.material.textfield.TextInputLayout> |
it would notify our “TextInputLayout” about value of product.weight in realtime so if we have entered an empty weight value or a value equals zero it would show an required error.
Another good thing is that because observables are embedded in model itself, it’s independent of any state change to our recycler view items we don’t have worry about the initiation or removal of recycler view items. but still we have to notify our recycler view in a way our old observables don’t stuck with our newly created views
RecyclerView Adapter and a ViewHolder
our viewHolder would look somewhat like this
class ViewHolder(var binding : ProductInfoBinding, | |
var context: Context) : | |
RecyclerView.ViewHolder(binding.root) { | |
fun bindTo(product : ProductObservable?) { | |
binding.apply { | |
binding.product = product | |
binding.lifecycleOwner = context as ProductActivity | |
} | |
} |
Callbacks And Activity ViewModel
Further by adding callbacks itself to the object we can update our activity ui on based of the state of data inside of adapter like this
private fun providePropertyChangedCallback(): OnPropertyChangedCallback { | |
return object : OnPropertyChangedCallback() | |
override fun onPropertyChanged(sender: Observable?, propertyId: Int) { | |
val product = sender as Product | |
viewModel.setDataMediatorValue ( | |
!(product.quantity == null || product.quantity == 0 || | |
product.weight.isNullOrEmpty() || | |
(product.weight ?: "0.00").toDouble() == 0.00 | |
) | |
} | |
} | |
} |
this call back is attached to the model via constructor and is passing state of data to the activtity view model
In the end this techniques help us to apply observable data just like liveData onto our recycler view items without binding it with the current state of item view
This article was originally published on proandroiddev.com on December 17, 2022