The Good, The Bad and The Ugly — Three approaches to loading contacts in your Android application

Daniel Dimovski
The Startup
Published in
8 min readApr 28, 2020

In this article we are going to take a deep dive into loading phone contacts in your Android application — we will explore different approaches to achieve this and analyze the pros and cons of each of these approaches.

Source

NOTE: All code excerpts require permission to read contacts and the first two need to be executed on a background thread in order to work. If you are not familiar with the concept of runtime permissions or background thread execution, follow the links.

The Bad

Recently I have been refactoring an application which, beside other functionalities and features, loads and displays phone contacts. This application was loading contacts using one of the most common approaches (if you check StackOverflow for this matter), and sadly, one of the worst approaches a developer can take to load phone contacts in Android. The code looked something like this (which is actually an accepted answer on StackOverflow with over 180 upvotes at the moment I am writing this post):

private void getContactList() {
ContentResolver cr = getContentResolver();
Cursor cur = cr.query(ContactsContract.Contacts.CONTENT_URI,
null, null, null, null);

if ((cur != null ? cur.getCount() : 0) > 0) {
while (cur != null && cur.moveToNext()) {
String id = cur.getString(
cur.getColumnIndex(ContactsContract.Contacts._ID));
String name = cur.getString(cur.getColumnIndex(
ContactsContract.Contacts.DISPLAY_NAME));

if (cur.getInt(
cur.getColumnIndex(
ContactsContract.Contacts.HAS_PHONE_NUMBER)) > 0) {
Cursor pCur = cr.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
ContactsContract.CommonDataKinds.Phone.CONTACT_ID
+ " = ?",
new String[]{id}, null);

while (pCur.moveToNext()) {
String phoneNo = pCur.getString(pCur.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.NUMBER));
Log.i(TAG, "Name: " + name);
Log.i(TAG, "Phone Number: " + phoneNo);
}
pCur.close();
}
}
}
if(cur!=null){
cur.close();
}
}

Looks familiar? If you run this code, it will work and it will give you results, but at what cost? If you take a better look at the code above, you will notice it queries the database once to get the contact ID for each contact, and then for each contact that has a phone number, it queries the database yet again to get contact details. Let’s say you have 1000 contacts in your phone and all of them have phone numbers. Running this code means you will query the database 1001 times (once to get all contacts and then once for each of the 1000 contacts). Seems like an overkill for fetching contacts from the phone, no?

Now, let’s up the ante and say your application is used by a busy business person that has 30000 contacts in his or her phone and your application runs the code above. It will take the application several minutes, depending on phone performance, to load the contacts from the phone and present it to your user. Yes, you read that right, I said minutes, not seconds. If you find a user that patient, I would like to meet him, or her.

Before, I said we were going to discuss pros and cons of each of these approaches. Well, there are no pros to this approach. The cons? Really slow, unnecessary complicated and wasteful, resource-wise. Managing a background thread seems insignificant among the other problems, but it is a problem

The Ugly

I know I am not following the order from the title, but I like building up from the bad and work our way to the good. Now, let’s look at the “ugly” approach. Although this may not be the most beautiful way to do the job, this approach has its qualities. Yes, this one isn’t all bad.

This approach removes the big problems of the previous one. We will not be querying the database too much, in fact, we will reduce the 1001 query to just two and still get all the information we need. “Why ugly”, you might ask? Well, because this approach’s qualities are on the inside, not the outside. But, don’t judge a book by its cover. Let’s take a look:

private void getContactsList() {
ContentResolver resolver = getContentResolver();
Map<Long, List<String>> phones = new HashMap<>();
Cursor getContactsCursor = resolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
new String[]{
ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
ContactsContract.CommonDataKinds.Phone.NUMBER},
null, null, null);

if (getContactsCursor != null) {
while (getContactsCursor.moveToNext()) {
long contactId = getContactsCursor.getLong(0);
String phone = getContactsCursor.getString(1);
List<String> list;
if (phones.containsKey(contactId)) {
list = phones.get(contactId);
} else {
list = new ArrayList<>();
phones.put(contactId, list);
}
list.add(phone);
}
getContactsCursor.close();
}
getContactsCursor = resolver.query(
ContactsContract.Contacts.CONTENT_URI,
new String[]{
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.PHOTO_URI},
null, null, null);
while (getContactsCursor != null &&
getContactsCursor.moveToNext()) {
long contactId = getContactsCursor.getLong(0);
String name = getContactsCursor.getString(1);
String photo = getContactsCursor.getString(2);
List<String> contactPhones = phones.get(contactId);
if (contactPhones != null) {
for (String phone :
contactPhones) {
addContact(contactId, name, phone, photo);
}
}
}
}

Doesn’t look the best, but it does the job very well. In short, we first query the database for list of all phone contacts and add them to a map. The key for the map is the contact id and the value is a list of the phone numbers the contact has (one contact can have more than one phone number, as we already know). Then we query the database once again, asking for details for the contacts, such as display name and photo uri in this case, and the contact id. Then we get the phone numbers for the contact id from the map and we that is it, we have all the details for the contact we need, by using only two queries.

If we run this code on the phone of that business person with 30000 contacts, it will execute within few seconds. I believe you will agree that’s quite an improvement.

Pros: Fetches contacts in relatively short time, doesn’t waste resources.

Cons: Manual work — handling background threads. Slower than “the good” approach.

The Good

Finally, let’s take a look at the last (and definitely not the least important) approach on our our list — the good one. This approach utilizes the Loader API which allows loading data from a content provider. This Loaders are executed on a background thread, so you don’t have to worry about managing threads, so that’s a good start.

It is good to mention that this approach is preferred by Google (for now) and is explained in detail in the official Android documentation. The instructions encourage the developers to use CursorAdapter, and you can do that, but in this example we will follow the same pattern as in the two previous examples, for the sake of consistency. The following code is a part of the sample application you can find at the end of this article.

This is how loading contacts looks like when using Loaders to do the job for you:

public class TheGoodFragment extends TheNeutralFragment implements LoaderManager.LoaderCallbacks<Cursor> {     public String[] PROJECTION_NUMBERS = new String[]   
{ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
ContactsContract.CommonDataKinds.Phone.NUMBER};
public String[] PROJECTION_DETAILS = new String[]
{ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.PHOTO_URI};
public static TheGoodFragment newInstance() {
return new TheGoodFragment();
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LoaderManager.getInstance(this).initLoader(0, null, this);
}

@NonNull
@Override
public Loader<Cursor> onCreateLoader(int id,
@Nullable Bundle args) {
startLoading();
switch (id) {
case 0:
return new CursorLoader(
getActivity(),
ContactsContract.CommonDataKinds.
Phone.CONTENT_URI,
PROJECTION_NUMBERS,
null,
null,
null
);
default:
return new CursorLoader(
getActivity(),
ContactsContract.Contacts.CONTENT_URI,
PROJECTION_DETAILS,
null,
null,
null
);
}
}

@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader,
Cursor data) {
switch (loader.getId()) {
case 0:
phones = new HashMap<>();
if (data != null) {
while (!data.isClosed() && data.moveToNext()) {
long contactId = data.getLong(0);
String phone = data.getString(1);
List<String> list;
if (phones.containsKey(contactId)) {
list = phones.get(contactId);
} else {
list = new ArrayList<>();
phones.put(contactId, list);
}
list.add(phone);
}
data.close();
}
LoaderManager.getInstance(TheGoodFragment.this)
.initLoader(1,null,this);
break;
case 1:
if (data!=null) {
while (!data.isClosed() && data.moveToNext()) {
long id = data.getLong(0);
String name = data.getString(1);
String photo = data.getString(2);
List<String> contactPhones =
phones.get(contactId);
if (contactPhones != null) {
for (String phone :
contactPhones) {
addContact(id, name, phone, photo);
}
}
}
data.close();
loadAdapter();
}
}
}

@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {

}
}

As you can see, the code is relatively well-structured (it definitely can be improved) and utilizes different callbacks. fWe are using two loaders, one to get the phone numbers from the database, and one to get the details for each contact, similar to what we did before. Each of the loaders has its id, so we can identify which loader has finished and which needs to be executed. Once the first loader finishes, we add the contacts in a HashMap and execute the second loader and repeat the process explained in the previous approach. And if you don’t want to deal with cursors, you can simply use a CursorAdapter, as I have previously mentioned.

Pros: The fastest of the three approaches, no need for manual background thread handling, doesn’t waste resources and is a well-structured approach.

Cons: Although it is currently the recommended way to load contacts from the phone, Google have declared Loaders deprecated as of Android P (API 28). In the future, we should be looking to utilize LiveData & ViewModels.

The Good Guy Wins

So, there we have it. The good, the bad, and the ugly approach to loading contacts in Android. Choosing the best method to load contacts can be vital when a user has a significant number of contacts in their phone. When loading a small number of contacts, you may not be able to tell the difference (after all, it’s a matter of milliseconds), so here’s a comparison between the three methods, when loading 200 contacts on Samsung Galaxy A20:

The Good: 28ms

The Bad: 3817ms

The Ugly: 114ms

Even for a relatively small number of contacts, you can easily tell the difference and even visually notice the delay when using ‘The Bad’ approach. Although you probably wouldn’t notice it visually, the ‘good’ approach is 4 times faster than the ‘ugly’ approach.

But, this test was too easy, right? Let’s try something more far-fetched. Let’s load 30000 contacts on an older device, such as Samsung A3. Let’s see the results:

The Good: 917 ms

The Bad: 334255 ms (about 5 and a half minutes)

The Ugly: 5930 ms

We are starting to notice the difference, visually. Now, even the most ignorant user will notice the time difference between the good and the ugly, let alone the bad approach.

Impressive, isn’t it, how a few lines of code can make a huge difference in performance?

Remember, (almost) everyone can copy and paste code from the Internet and put together an application, thus calling him or herself a ‘programmer’. You don’t have to be an expert to do that. The difference between a software engineer and a ‘programmer’ is being able to tell the difference between various approaches (nay, algorithms), evaluate them and adapt them, or even come up with a new approach, so that the application executes quickly and efficiently, even when it comes to something as tedious as loading contacts from a phone.

I have put together (not developed) a small application to let you test the three different approaches and see the difference for yourself. You can find it on GitHub.

Disclaimer: The application is just an example, it is not well-structured, nor does it use all the latest trends in Android development (i.e. don’t use AsyncTasks, they are deprecated). Please treat it as such and don’t copy-paste the code in your own application.

TL;DR: Use Loaders for loading contacts into your Android application. Don’t copy-paste stuff off the Internet just to make things work. Think before you code.

Follow me for more similar articles related to software engineering and Android development.

--

--