Introduction
If your app needs to display a scrolling list of elements based on large data sets (or data that frequently changes), you should use [RecyclerView
](developer.android.com/reference/androidx/re..) as described on this page.
Sometimes setting up RecyclerView can be complicated, but the arrival of databinding has made the process of setting up RecyclerView a little easier.
In this tutorial, we will try to display data from an API in recyclerView using the databinding mechanism.
The result
Creating the entity to be displayed in the RecyclerView
We will try to create a small class that represents the data that will be displayed in the recyclerView.
@Parcelize
data class User(
val id: Long,
val avatar_url: String,
val login: String
) : Parcelable
After creating the entity, we have to create an interface that will allow us to use the API. We will use the data coming from this URL api.github.com/users. The getUserAsync method uses the keyword suspend to take care of the coroutines. To make it work, you need to have retrofit 2.6 or higher. For more info about using retrofit with the coroutine, I advise you to have a look at this article link.
interface UserApi {
@GET("users")
suspend fun getUserAsync(): Response<List<User>>
}
The ViewModel
For the sake of simplicity, the small application that we will develop will be able to get its information only from a single source. In our case, it will only be the API, that’s why the UserViewModel takes as a parameter and object of type UserApi.
class UserViewModel(private val userApi: UserApi) : ViewModel() {
private val _loading = MutableLiveData<LoadingState>()
val loading: LiveData<LoadingState>
get() = _loading
private val _data = MutableLiveData<List<User>>()
val data: LiveData<List<User>>
get() = _data
init {
fetchData()
}
private fun fetchData() {
viewModelScope.launch(Dispatchers.IO) {
try {
_loading.postValue(LoadingState.LOADING)
val response = userApi.getUserAsync()
if (response.isSuccessful) {
_data.postValue(response.body())
_loading.postValue(LoadingState.LOADED)
} else {
_loading.postValue(LoadingState.error(response.message()))
}
} catch (e: Exception) {
_loading.postValue(LoadingState.error(e.message))
}
}
}
}
To get an idea of the loading status, I created a small utility class, the LoadingState class.
@Suppress("DataClassPrivateConstructor")
data class LoadingState private constructor(val status: Status, val msg: String? = null) {
companion object {
val LOADED = LoadingState(Status.SUCCESS)
val LOADING = LoadingState(Status.RUNNING)
fun error(msg: String?) = LoadingState(Status.FAILED, msg)
}
enum class Status {
RUNNING,
SUCCESS,
FAILED
}
}
Connecting the ViewModel to the activity
Once the ViewModel is ready, we now need to link it to our activity using the instruction by viewModel<UserViewModel>() provided by koin.
class MainActivity : AppCompatActivity() {
private val userViewModel by viewModel<UserViewModel>()
private val binding by lazy {
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main).run {
lifecycleOwner = this@MainActivity
viewModel = userViewModel
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userViewModel.data.observe(this, Observer {
Log.e("MainActivity", it.toString())
})
}
}
As you can see, the activity is linked to the layout using the generic method setContentView of the DataBindingUtil class which has as type parameter the ActivityMainBinding class. The reference of the current activity and the layout that will be attached to the activity, the ActivityMainBinding class is generated from the layout activity_main.xml.
The following code returns an object of type ActivityMainBinding which has a lifecyclerOwner property to which we have assigned the reference of the current activity and another viewModel property to which we have assigned the viewModel we have recently retrieved. This last property is a variable declared in the file activity_main.xml as we will see in the rest of the post.
private val binding by lazy {
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main).run {
lifecycleOwner = this@MainActivity
viewModel = userViewModel
}
}
For the ActivityMainBinding class to be generated, you have to activate databinding in the build.gradle file of your module.
// In order to have parcelize annotation
androidExtensions {
experimental = true
}
android {
dataBinding {
enabled = true
}
// The rest of the file
}
Application dependencies
If you try to compile the application at this level you will get an error because until then, you haven’t created an object of type UserApi as given as a parameter to the UserViewModel class. In other words, you have to create all the Koin modules necessary for the correct operation of the application.
The previous file contains 3 modules, the first netModule, contains all the dependencies for retrofit, the second retrofitServiceModule returns an instance of type UserApi, the class we used in the UserViewModel class, and for the last module viewModelModule which returns the only viewModel of our small application.
If you still have some problems using Koin I advise you to take a look at an article I wrote on the subject at this link
Start the Koin container
Once startKoin
has been called, Koin will read all your modules & definitions. Koin is then ready for any get()
or by inject()
call to retrieve the needed instance.
We now need to link this class to the manifest of our application, in the application element using the android:name attribute. This way, the class App will be launched at the startup of the application.
Don’t forget to allow the application to connect to the internet so that retrofit can easily retrieve data from our API.
At this step, you can already compile the application. If you have followed the steps until now, you will see the data coming from the API in the LogCat in Android Studio.
Connect data to recycleView via databinding
To display the data in the RecyclerView, you must first create an XML file that will represent an element of the list. If you are careful, you will notice that the User class has only 3 properties, the id, the login and the avatar of the user. We are going to use only the login and the avatar_url.
The XML code in the item_user.xml file looks like this
If we analyze this file very well, the first thing we noticed is that the ConstraintLayout is in a layout tag, and at the beginning of the file we declared a variable of type User (the User class we had created above)
The text attribute of the TextView is initialized by the value of the login property by using a structure like “@{user.login}”. You use this notation whenever you need to retrieve the value of a variable declared in a layout.
To have access to the layout element, you need to enable databinding in your build.gradle file.
Binding Adapter
The image view is linked to the user’s avatar URL using the setImageUrl attribute of the namespace app, if you’ve ever had to use ImageViews before you notice that this attribute doesn’t exist for an ImageView, yes it’s true this attribute doesn’t exist as long as you add it.
To create a new attribute to a component, in our case the ImageView, you have to create a structure like this one.
We created an extension function of the ImageView class, which takes as parameter the URL of the image to load, then we used the annotation BindingAdapter to define the name of the attribute that will be used in the XML file.
We just created a new attribute for the ImageView, this means that every time we start using the setImageUrl attribute, the value of this attribute will be passed to the bindImageUrl function which will use it to load the image from the URL. In our case, we used Picasso to load the image from the URL.
RecyclerView Binding Adapter
We will use the same principle to define an attribute that we start using to link the adapter to the recyclerView directly from the XML file.
The RecyclerView in the XML will have access to the setAdapter attribute to which will be assigned an adapter that will link to the RecyclerView.
RecyclerView Adapter
In order to be able to display the data in the recyclerView, you need an adapter, in our case we used a ListAdapter.
As you can see I didn’t use a classic adapter, I used the ListAdapter that came with Jetpack, but it will also work with a classic adapter,
The UserAdapter class contains an internal class which takes a binding property of type ItemUserBinding, and which inherits from the RecylerView.ViewHolder class.
I think it tells you something about the ItemUserBinding class. It was generated from the layout item_user.xml. In this layout, we declared a variable of type user, that’s why in the onBindViewHolder method I can have access to the user property of the binding of our viewHolder that we initialize by the value returned by the getItem(position) method.
At the moment, we declare a variable in XML it doesn’t have a value yet, that’s why we had to initialize the user declarer property in the item_user.xml file in the onBindViewHolder method.
With the ListAdapter, we retrieve the data at a given position by calling the getItem(position) method. This method returns an object of the same type as the one we used as the first type parameter of the UserAdapter class in our case it’s User. If you pay attention, you will notice the ListAdapter class has two type parameters. The first one represents the class that will be managed by the list, and the second one is the viewHolder.
In the onCreateViewholder method, we can retrieve the layout attached to the ItemUserBinding class by calling its inflate method by passing an object of type LayoutInflater. This method returns an object of type ItemUserbinding we can use to create an instance of type UserViewHolder.
Linking RecyclerView to Adapter
The layout of the activity main is simple, because it has only two elements, a ProgressBar and a RecyclerView, with two variables. The first variable has the type of adapter we recently created so UserAdapter, and the second variable has the same type as the viewModel (UserViewModel).
The RecyclerView is linked to the adapter on the line app:setAdapter=”@{adapter}”, note that the attribute setAdapter has been created above with the annotation BindingAdapter.
We took the same technique for the ProgressBar which will start to be displayed according to the state of data loading by the attribute app:setupVisibility=”@{viewModel.loading}” which was created by the following codes.
The object returned by the loading attribute of the viewModel is of type LoadingState, this object will be passed to the method progressVisibility which is an extension of the ProgressBar class.
To finish, you now have to pass the data to the layout activity_main.xml from the activity, firstly the adapter and the UserViewModel, after that we have just to observe data from ViewModel and passing it to the adapter using submitList method of the listAdapter.
class MainActivity : AppCompatActivity() {
private val userViewModel by viewModel<UserViewModel>()
private val binding : ActivityMainBinding by lazy {
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main).apply {
lifecycleOwner = this@MainActivity
viewModel = userViewModel
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val userAdapter = UserAdapter()
binding.adapter = userAdapter
// Observe data from viewModel
userViewModel.data.observe(this, Observer {
Log.e("MainActivity", it.toString())
it.let(userAdapter::submitList)
})
}
}
Conclusion
Databinding with the RecyclerView is not always easy to understand at first, feel free to leave a comment if you want some clarification about a step in the tutorial.
If you have any kind of feedback, feel free to connect with me on Twitter.
Reference
Binding adapters | Android Developers Binding adapters are responsible for making the appropriate framework calls to set values. One example is setting a…developer.android.com