Loading data from ContentProvider using Coroutines and LiveData
Most of the ContentProvider tutorials shows us loading data using CursorLoader. As android development is moving away from Loaders and asyncTasks, I think we should consider implementing these things using alternatives like coroutine. Through this story we’ll be trying to load data from content provider using coroutine and show it on a list.
We will build a sample app which load contacts from content provider trying to follow the recommended app architecture. You can replace it with any content provider you want. Before starting to code let’s add the dependencies for Coroutines, livedata-ktx and gilde.
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
Generally we would have been using a cursor loader for loading the contacts from the content provider. But here we will be using Coroutines which are basically a light weighted thread that is available in kotlin. LiveData ktx is a kotlin extension for livedata which helps us to convert data loaded in the coroutine block into a LiveData. You can read more about it here. We will be using Glide library for showing the contact images on an imageview.
Now let’s start coding. First we have to setup our model class and data source.
data class MyContact(val name: String, val image: String)
Our data source will be like this:
class MyContactsDataSource(private val contentResolver: ContentResolver) {
fun fetchContacts(): List<MyContact> {
val result: MutableList<MyContact> = mutableListOf()
val cursor = contentResolver.query(
ContactsContract.Data.CONTENT_URI,
arrayOf(
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.CONTACT_ID
),
null,
null,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME
)
cursor?.let {
cursor.moveToFirst()
while (!cursor.isAfterLast) {
result.add(
MyContact(
cursor.getString(0),
cursor.getString(1).toContactImageUri()
)
) //add the item
cursor.moveToNext()
}
cursor.close()
}
return result.toList()
}
}
fun String.toContactImageUri() = Uri.withAppendedPath(
ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, this.toLong()),
ContactsContract.Contacts.Photo.CONTENT_DIRECTORY
).toString()
Nothing fancy is happening here in the data source. We are loading cursor which contains DISPLAY_NAME and CONTACT_ID from the CONTENT_URI for contacts to a list of MyContact object. The extension method toContactImageUri will convert CONTACT_ID to the path of contact image. As you can see here we are using the uri for contacts, you can replace it with any content provider uri which you will be loading.
And now the repository will be like this:
class MyContactsRepository(private val source: MyContactsDataSource, private val myDispatcher: CoroutineDispatcher) {
suspend fun fetchContacts(): List<MyContact> {
return withContext(myDispatcher) {
source.fetchContacts()
}
}
}
Here withContext
lets you control what thread source.fetchContacts() executes on. myDispatcher will be the CoroutineDispatcher on which fetchContacts() will run. As we are doing an IO operation we’ll be using Dispatchers.IO. This will keep us off the main thread while doing DB operations. Instead of hardcoding our dispatcher in the repository, we’ll be passing it through the constructor to make testing easier. I have covered more about testing in this article.
From MainViewModel, we call our repository for fetching the data.
class MainViewModel(
context: Application,
private val myContactsRepository: MyContactsRepository
) : AndroidViewModel(context) {
var myContacts: LiveData<List<MyContact>> = liveData {
emit(myContactsRepository.fetchContacts())
}
}
Here we are using the liveData builder which returns a LiveData. We will be able to run a coroutine block inside this and can emit the results to a LiveData. The coroutine block will start running after an observer is attached to the LiveData. More details on liveData builder can be found in this link.
Let’s create our viewmodel factory class for creating our viewmodels.
class MyViewModelFactory(private val application: Application) :
AndroidViewModelFactory(application) {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
val source =
MyContactsDataSource(application.contentResolver)
MainViewModel(application, MyContactsRepository(source, Dispatchers.IO)) as T
} else
throw IllegalArgumentException("Unknown ViewModel class")
}
}
That’s it, we have created our viewmodel. We can fetch the content provider data from our views. Let’s say we are fetching the data from an activity and populating it onto a recyclerview. First we have to create an adapter for our recyclerview for this.
class MyContactsAdapter : ListAdapter<MyContact, ViewHolder>(DiffCallback()) {
// Inflates the item views
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.contact_list_item,
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvImage: ImageView = itemView.ivCharacter
private val tvName: TextView = itemView.tvName
fun bind(item: MyContact) {
tvName.text = item.name
Glide.with(tvImage)
.load(item.image)
.into(tvImage)
}
}
class DiffCallback : DiffUtil.ItemCallback<MyContact>() {
override fun areItemsTheSame(oldItem: MyContact, newItem: MyContact): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: MyContact, newItem: MyContact): Boolean {
return oldItem == newItem
}
}
Our contact_list_item will be:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/itemParent"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/ivCharacter"
android:layout_width="100dp"
android:layout_height="100dp"
android:contentDescription="@string/app_name"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvName"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/ivCharacter"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
And our activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/parentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#777"
tools:context=".main.MainActivity">
</androidx.recyclerview.widget.RecyclerView>
In our activity we will be observing the data inside our viewmodel.
class MainActivity : AppCompatActivity() {
private val adapter = MyContactsAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val myViewModelFactory =
MyViewModelFactory(application)
val viewModel = ViewModelProvider(this, myViewModelFactory).get(MainViewModel::class.java)
setContentView(R.layout.activity_main)
parentView.layoutManager = LinearLayoutManager(this)
parentView.adapter = adapter
viewModel.myContacts.observe(this, Observer {
adapter.submitList(it)
})
}
}
Conclusion
Using coroutine and LiveData we can keep our activities lean and we don’t have bloat it with all the loader callbacks.
Note: Please add the necessary runtime permissions if you are using the contacts.