Android
Address Book
Data Retrieval
Performance Optimization
Mobile Development

How can get data from address-book faster in Android?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Slow contact loading in Android apps is usually caused by inefficient query patterns rather than raw device speed. Many implementations fetch too many columns, run content resolver calls on the main thread, or issue repeated nested queries for each contact. Faster retrieval comes from one efficient query plan, background execution, and minimal transformation work before first render.

Start With a Lean Projection

Contacts provider exposes many fields, but list screens usually need only a few. Query only required columns and avoid heavy blobs such as photos during initial load.

kotlin
1val projection = arrayOf(
2    ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
3    ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
4    ContactsContract.CommonDataKinds.Phone.NUMBER
5)
6
7val sortOrder = ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " COLLATE LOCALIZED ASC"
8
9val cursor = contentResolver.query(
10    ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
11    projection,
12    null,
13    null,
14    sortOrder
15)

Reducing projection size often gives immediate gains on large address books.

Run Queries Off the Main Thread

Content provider operations can block UI rendering. Use coroutines on Dispatchers.IO and return mapped data back to UI thread.

kotlin
1data class ContactRow(
2    val id: Long,
3    val name: String,
4    val number: String
5)
6
7suspend fun loadContacts(resolver: ContentResolver): List<ContactRow> = withContext(Dispatchers.IO) {
8    val result = mutableListOf<ContactRow>()
9
10    val projection = arrayOf(
11        ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
12        ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
13        ContactsContract.CommonDataKinds.Phone.NUMBER
14    )
15
16    resolver.query(
17        ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
18        projection,
19        null,
20        null,
21        ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC"
22    )?.use { c ->
23        val idIx = c.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)
24        val nameIx = c.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
25        val numIx = c.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER)
26
27        while (c.moveToNext()) {
28            result.add(
29                ContactRow(
30                    id = c.getLong(idIx),
31                    name = c.getString(nameIx) ?: "",
32                    number = c.getString(numIx) ?: ""
33                )
34            )
35        }
36    }
37
38    result
39}

The use block prevents cursor leaks and keeps memory stable.

Avoid Nested Per-Contact Queries

A common slow pattern is querying contact IDs first, then querying numbers separately for each ID. That creates many provider calls and poor scaling.

Prefer querying CommonDataKinds.Phone once and deduplicating in memory if needed.

kotlin
1fun keepFirstNumberPerContact(rows: List<ContactRow>): List<ContactRow> {
2    val map = LinkedHashMap<Long, ContactRow>()
3    for (row in rows) {
4        if (!map.containsKey(row.id)) {
5            map[row.id] = row
6        }
7    }
8    return map.values.toList()
9}

This keeps one query path and predictable runtime.

Cache, Observe, and Incrementally Refresh

If contacts are shown frequently, cache transformed rows in memory or local database. Register content observer to invalidate cache on changes.

High-level strategy:

  1. Load cached contacts immediately for fast first paint.
  2. Refresh in background from provider.
  3. Update cache and UI if data changed.

This gives responsive UX even when provider query is slow.

Permission and Lifecycle Handling

Contact reads require runtime permission checks. Handle permission denial quickly and avoid retry loops that block UI progress.

Also tie loading jobs to lifecycle-aware scopes such as viewModelScope. Cancel work when screen closes to avoid stale updates and unnecessary CPU usage.

kotlin
1viewModelScope.launch {
2    if (!hasContactsPermission()) {
3        _state.value = UiState.PermissionRequired
4        return@launch
5    }
6
7    val rows = loadContacts(contentResolver)
8    _state.value = UiState.Ready(rows)
9}

Measure and Verify Performance

Track timing around query and mapping stages, not only total screen load time.

Metrics worth logging:

  • query duration
  • row count
  • transform duration
  • time to first list item rendered

Measure before and after each change. Optimization without measurement usually shifts work instead of reducing it.

Common Pitfalls

A common pitfall is selecting too many unused columns, which inflates provider work and memory pressure. Another is querying on main thread, causing jank and potential ANR errors. Nested queries per contact are also frequent and scale poorly. Teams sometimes leak cursors by skipping use or close in exception paths. Finally, many apps reload full contacts on each screen entry without caching or observer-based invalidation. If the contact list is central to the product, the loading strategy should be treated as data access design, not UI glue code.

Summary

  • Use minimal projections and targeted query endpoints.
  • Run contact queries on background dispatchers.
  • Prefer one broad query over nested per-contact requests.
  • Deduplicate in memory when UI needs one row per contact.
  • Cache results and refresh on content changes.
  • Profile query and mapping time to validate real improvements.

Course illustration
Course illustration

All Rights Reserved.