4、读取联系人列表:查询ContactsContract.Contacts表、获取联系人ID与姓名、使用CursorLoader异步加载、适配器展示数据
好,咱们今天来啃一块硬骨头——读取系统联系人列表。
说实话,这个功能我最早做的时候,踩了不少坑。那时候刚接触Android开发,直接在主线程查数据库,结果界面卡得跟幻灯片似的。后来才明白,联系人数据查询必须异步处理。嗯,这节课咱们就把这件事彻底讲透。
4.1 认识ContactsContract.Contacts表
Android系统把联系人数据存在一个叫ContactsContract的内容提供器里。这里面有很多表,但最核心的就是ContactsContract.Contacts。
这张表存的是什么?说白了,就是每个联系人的“概要信息”。每条记录代表一个联系人,包含联系人ID、显示名称、是否有电话号码等基础字段。
| 字段名 | 类型 | 说明 |
|---|---|---|
_ID |
long | 联系人唯一标识,后面关联其他表全靠它 |
DISPLAY_NAME |
String | 联系人显示名称,就是你在通讯录里看到的名字 |
HAS_PHONE_NUMBER |
int | 0表示无号码,1表示有号码。我经常用它做过滤 |
PHOTO_URI |
String | 头像URI,可以为空 |
我个人习惯,每次查询联系人时都会带上HAS_PHONE_NUMBER=1这个条件。为什么?因为很多应用只关心有电话号码的联系人,比如拨号器、短信应用。你想想看,查出一堆没有号码的联系人,展示出来也没意义。
4.2 查询联系人ID与姓名
查询Contacts表其实很简单,就是一次ContentResolver查询。但要注意,我们得用ContentResolver.query()方法,传入正确的URI和投影列。
先看一个基础版本:
// 定义投影列,只查我们需要的字段
String[] projection = {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.HAS_PHONE_NUMBER
};
// 查询条件:只查有电话号码的联系人
String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = ?";
String[] selectionArgs = new String[]{"1"};
// 执行查询
Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
projection,
selection,
selectionArgs,
ContactsContract.Contacts.DISPLAY_NAME + " ASC" // 按姓名排序
);
if (cursor != null) {
while (cursor.moveToNext()) {
long contactId = cursor.getLong(cursor.getColumnIndex(ContactsContract.Contacts._ID));
String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
// 这里就可以把数据加到列表里了
}
cursor.close();
}
这段代码能跑,但有个大问题——它在主线程执行。我曾经在一个老项目里看到过类似的写法,用户通讯录里有2000多个联系人,结果界面卡了足足3秒。用户直接给了一星差评。
4.3 使用CursorLoader异步加载
那怎么解决异步问题?官方推荐的方式是用CursorLoader。它配合LoaderManager,可以在后台线程查询数据,查询完成后自动把结果送到UI线程。
我个人觉得,CursorLoader的设计思路很优雅。它会在Activity或Fragment的生命周期内自动管理Cursor,不会泄漏,也不会重复查询。你想想看,如果每次屏幕旋转都重新查一遍数据库,那体验得多糟糕。
来看具体实现:
public class ContactListFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
private static final int CONTACT_LOADER_ID = 1;
private SimpleCursorAdapter adapter;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// 初始化Loader
getLoaderManager().initLoader(CONTACT_LOADER_ID, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// 定义查询的URI和投影
Uri uri = ContactsContract.Contacts.CONTENT_URI;
String[] projection = {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.HAS_PHONE_NUMBER
};
String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = ?";
String[] selectionArgs = new String[]{"1"};
String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " ASC";
// 创建CursorLoader,自动在后台线程执行
return new CursorLoader(
getActivity(),
uri,
projection,
selection,
selectionArgs,
sortOrder
);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// 数据加载完成,把Cursor交给适配器
adapter.swapCursor(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
// Loader被重置时,释放旧的Cursor
adapter.swapCursor(null);
}
}
这里有个细节要注意:onLoadFinished是在UI线程回调的,所以你可以放心地更新界面。swapCursor方法会关闭旧的Cursor并换上新数据,非常安全。
CursorLoader的替代方案——LiveData配合Room。但如果是维护老项目,CursorLoader依然是最稳妥的选择。我在一个2015年的项目里,到现在还在用CursorLoader,稳定运行了8年没出过问题。
4.4 适配器展示数据
数据查出来了,怎么展示?最直接的方式是用SimpleCursorAdapter。它能把Cursor里的字段自动映射到ListView或RecyclerView的Item布局上。
先定义Item布局文件contact_list_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<TextView
android:id="@+id/contact_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp" />
<TextView
android:id="@+id/contact_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="#999999" />
</LinearLayout>
然后创建适配器:
// 定义从Cursor到视图的映射
String[] fromColumns = {
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts._ID
};
int[] toViews = {
R.id.contact_name,
R.id.contact_id
};
// 创建适配器
adapter = new SimpleCursorAdapter(
getActivity(),
R.layout.contact_list_item,
null, // 初始时没有Cursor,后面通过swapCursor设置
fromColumns,
toViews,
0 // flags,传0即可
);
// 设置到ListView
ListView listView = findViewById(R.id.contact_list);
listView.setAdapter(adapter);
你看,代码量其实不多。但这里有个坑——SimpleCursorAdapter默认情况下,如果Cursor里某个字段为null,它会显示"null"字符串。我曾经因为这个被测试提过bug,说联系人列表里出现了"null"字样。解决方案是自定义一个ViewBinder:
adapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
@Override
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
if (view.getId() == R.id.contact_name) {
String name = cursor.getString(columnIndex);
if (name == null || name.isEmpty()) {
((TextView) view).setText("未知联系人");
} else {
((TextView) view).setText(name);
}
return true; // 表示我们已经处理了这个视图
}
return false; // 其他视图让适配器自己处理
}
});
- ContactsContract.Contacts表存的是联系人概要信息,核心字段是_ID和DISPLAY_NAME
- 查询时必须异步,CursorLoader是官方推荐的解决方案
- SimpleCursorAdapter可以快速实现Cursor到ListView的映射
- 记得处理null值,避免显示"null"字符串
最后,我画了一张流程图,帮你理清整个数据流向:
整个流程其实就五步:用户进入页面 → 初始化Loader → 后台查询 → 回调返回数据 → 更新列表。每一步都有明确的职责,环环相扣。
好了,这节课的内容就到这里。记住,读取联系人列表的核心就是异步加载,千万别在主线程里直接查数据库。CursorLoader虽然看起来代码多一些,但它帮你处理了生命周期、线程切换、Cursor管理等一堆麻烦事,值得你花时间掌握。
公众号:蓝海资料掘金营,微信deep3321