Blog Infos
Author
Published
Topics
Published
Topics

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
  1. 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
  2. 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
  3. 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,
)
view raw data.kt hosted with ❤ by GitHub

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}" />
view raw product.xml hosted with ❤ by GitHub

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>
view raw product.xml hosted with ❤ by GitHub

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)
}
}
view raw product.kt hosted with ❤ by GitHub

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

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>
view raw item.xml hosted with ❤ by GitHub

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
}
}
view raw viewholder.kt hosted with ❤ by GitHub
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
)
}
}
}
view raw callback.kt hosted with ❤ by GitHub

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

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu