Table of contents
No headings in the article.
In this article we are going to have a deep dive into importing phone contacts into an Android application. We’ll look at two methods for doing this and weigh their pros and cons.
NOTE: All code snippets require the ability to read contacts through permission. Follow the links if you are unfamiliar with the idea of runtime permissions or background thread execution.
The Noob Approach:
I have recently been working on an application that loads and shows phone contacts in addition to other features and functionalities. If you examine StackOverflow for this topic, this application was loading contacts using one of the most popular methods — and regrettably, one of the bad methods a developer may use to load phone contacts on Android.
Let’s look at the code then!
We will be needing READ_CONTACTS permission in both approaches. We need to declare this permission in main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- add READ_CONTACTS permission here -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ContactBook"
tools:targetApi="31">
<activity
android:name=".ui.home.HomeActivity"
android:exported="true"
android:label="@string/title_activity_home">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</application>
The following noob approach looks familiar? This code will function and provide results if you run it, but at what cost? If you look closely at the code above, you will see that the loop will go through all the contacts of android device.In every iteration, it will fetch name and phone number in different variables.Then, it will store both information in object of contactModel class and finally, object is passed through the adapter.
Now Assume you have 1000 contacts on your phone, all of which are reachable via phone. You will query the database 1001 times if you run this code (once to get all contacts and then once for each of the 1000 contacts). Seems excessive for retrieving contacts from the phone, don’t you think?
class NoobFragment : Fragment() {
private var _binding: FragmentNoobBinding? = null
private val mViewBinding get() = _binding!!
private var adapter: ContactListAdapter? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
....................
}
@SuppressLint("Range")
private fun callContact(){
val phones = context?.contentResolver?.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
null,
null,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC"
)
while (phones!!.moveToNext()) {
val name =
phones.getString(phones.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
val phoneNumber =
phones.getString(phones.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
val contactModel = ContactModel(name, phoneNumber)
adapter?.setData(contactModel)
}
(activity as HomeActivity?)?.getLoadingTime("Noob approach execution time: ")
mViewBinding.loader.visibility=View.GONE
phones.close()
}
}
Let’s boost the ante and assume that a busy person who has 30,000 contacts on their phone uses your application, which executes the aforementioned code. Depending on the performance of the phone, it may take the program several minutes to load the contacts from the phone and display them to the user. Yes, minutes, not seconds — you read it properly. I would want to meet a user who is that patient, if you can locate one.
I previously stated that we would talk about the pros and cons of each of these approaches. Well, this one has no pros. The cons? Really slow, overly convoluted, and resource-wasting. In comparison to the other issues, managing a background thread may seem unimportant, yet it can also result in a lack of view.
The Pro Approach:
Let’s now examine the last strategy on our list — the good one and mostly a pro one — which is certainly not the least significant. This method makes use of the Loader API, which enables data to be loaded from a content source. That’s a good start because these loaders are done on a background thread, which means you don’t have to worry about managing threads.
It is important to note that this strategy is chosen by Google (for the time being) and is thoroughly described in the official Android documentation. Although using CursorAdapter is encouraged in the instructions, we’ll stick to the same approach as in the previous preceding instances in this example for consistency’s sake. The sample application, which you may access at the conclusion of this post, includes the code listed below.
This is how loading contacts looks like when using Loaders to do the job for you:
class ProFragment : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
private var _binding: FragmentProBinding? = null
var PROJECTION_NUMBERS = arrayOf(
ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
ContactsContract.CommonDataKinds.Phone.NUMBER
)
var PROJECTION_DETAILS = arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.PHOTO_URI
)
var phoneMap: Map<Long, ArrayList<String?>> = HashMap()
private var adapter: ContactListAdapter? = null
private val mViewBinding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
........................
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
(activity as HomeActivity?)?.startLoading()
return when (id) {
0 -> CursorLoader(
requireActivity(),
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
PROJECTION_NUMBERS,
null,
null,
null
)
else -> CursorLoader(
requireActivity(),
ContactsContract.Contacts.CONTENT_URI,
PROJECTION_DETAILS,
null,
null,
null
)
}
}
override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {
mViewBinding.loader.visibility=View.GONE
when (loader.id) {
0 -> {
phoneMap = HashMap()
if (data != null) {
while (!data.isClosed && data.moveToNext()) {
val contactId: Long = data.getLong(0)
val phone: String = data.getString(1)
var list: ArrayList<String?> = ArrayList()
if (phoneMap.containsKey(contactId)) {
phoneMap[contactId]?.let { list.addAll(it) }
} else {
list = ArrayList()
(phoneMap as HashMap<Long, ArrayList<String?>>)[contactId] = list
}
list.add(phone)
}
data.close()
}
LoaderManager.getInstance(requireActivity())
.initLoader(1, null, this)
}
1 -> if (data != null) {
while (!data.isClosed && data.moveToNext()) {
val contactId: Long = data.getLong(0)
val name: String = data.getString(1)
val contactPhones: ArrayList<String?>? = phoneMap[contactId]
if (contactPhones != null) {
for (phone in contactPhones) {
adapter?.setData(ContactModel(name, phone))
}
}
}
data.close()
(activity as HomeActivity?)?.getLoadingTime("Pro approach execution time: ")
}
}
}
override fun onLoaderReset(loader: Loader<Cursor>) {
}
}
As you can see, the code makes use of a variety of callbacks and is generally well-structured (although it may be improved). We are utilising two loaders: one to retrieve the phone numbers from the database and the other to retrieve the information for each contact. Each loader has a unique ID, allowing us to distinguish between those that have completed and those that still need to be processed. After the first loader completes, we add the contacts to a HashMap, run the second loader, and then repeat the steps outlined in the prior technique. Additionally, as I’ve already indicated, you may use a CursorAdapter if you don’t want to deal with cursors.
Pros: This method is the fastest than the previous, requires no manual background thread handling, is resource-efficient, and is well-organized.
Cons: Google has deemed loaders deprecated as of Android P, despite the fact that it is now the recommended method for loading contacts from Android P(API 28). We should aim to use LiveData & ViewModels in the future.
To help you test the two approaches and see the differences for yourself, I have uploaded a tiny application. It’s available on this GiHub Repository.
Disclaimer: The application is only meant to be used as an example and does not adhere to the best practices for Android development. Do not copy-paste code from Internet into your own project. Instead, think before to how to use it.
Follow me for more articles regarding Android development.
Happy Coding :)
Thanks for reading my article. If you found it valuable, follow me on Medium and LinkedIn for more updates on Android development. I’ll be sharing new articles and resources to help improve your process and deliver better products. Thanks again for reading, and I look forward to connecting with you!