多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

contact 基础知识总结与类图:Fragment 中 LoaderManager 与 CursorLoader 配置骨架

contact 基础知识总结与类图:Fragment 中 LoaderManager 与 CursorLoader 配置骨架 1. 从一次联系人列表卡顿说起Android 联系人模块里Fragment、LoaderManager、AsyncLoader、CursorLoader这几个类经常一起出现但真正落到代码里很多人会卡在“到底谁初始化谁、回调为什么不触发、类图怎么画才不绕”。这篇就聚焦 Android 联系人模块的基础骨架在Fragment里用LoaderManager驱动AsyncLoader/CursorLoader读取联系人并给出可复制的初始化代码、CursorLoader参数配置以及一张能看懂的类图字段与关系标注。如果你正在做MultiSelectContactsListFragment、AllContactsFragment、SmartDialSearchFragment、RegularSearchFragment这类界面或者只是想把getLoaderManager().initLoader()的调用链理顺这篇可以直接跟着敲。核心检索词先摆出来Fragment中通过LoaderManager管理AsyncLoaderCursorLoader负责在后台线程查询ContactsContract查询结果通过onLoadFinished回到主线程刷新列表。适合已经会写RecyclerView、但对 Loader 三件套回调顺序模糊的 Android 开发者。我试过在联系人搜索页里把 Loader 初始化放在onCreateView和onActivityCreated两个位置结果回调触发时机完全不同后面会把这个坑单独拆开讲。2. TaoToken 前置把模型对话和接入文档放在手边写这类系统模块代码时我习惯把 API 文档和模型对话窗口开着遇到ContactsContract字段记不清、或者CursorLoader构造参数顺序拿不准直接问比翻源码快。TaoToken 这边可以这样用模型对话入口用来快速确认ContactsContract.CommonDataKinds.Phone.CONTENT_URI这类常量的完整路径以及LoaderManager在不同 API Level 下的行为差异。接入文档里能查到 API Key 的生成方式和请求示例方便你在本地写一个最小验证脚本确认查询条件拼得对不对。模型对话https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteAPI Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite如果你后面要长期做 Android 编码或 Agent 类工具链可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite注意TaoToken 是模型调用与文档查询的辅助入口不替代 Android Studio 和真机调试。Loader 回调是否触发最终还是要看 Logcat。3. 可复制配置LoaderManager 初始化与 CursorLoader 参数骨架3.1 Fragment 中初始化 LoaderManager三要素关系先理清Fragment或Activity持有LoaderManagerLoaderManager管理一个或多个LoaderAsyncLoader是抽象基类CursorLoader是它的子类。初始化入口就一行getLoaderManager().initLoader(LOADER_ID, null, this);这里LOADER_ID是自定义常量this要求当前Fragment实现LoaderManager.LoaderCallbacksCursor。完整骨架如下public class ContactsListFragment extends Fragment implements LoaderManager.LoaderCallbacksCursor { private static final int LOADER_ID_CONTACTS 1001; private RecyclerView recyclerView; private ContactsAdapter adapter; Override public void onActivityCreated(Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // 在视图创建完成后初始化避免回调早于 RecyclerView 就绪 getLoaderManager().initLoader(LOADER_ID_CONTACTS, null, this); } NonNull Override public LoaderCursor onCreateLoader(int id, Nullable Bundle args) { if (id ! LOADER_ID_CONTACTS) { throw new IllegalArgumentException(unknown loader id: id); } String[] projection new String[]{ ContactsContract.CommonDataKinds.Phone._ID, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER }; String selection ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME IS NOT NULL; String sortOrder ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME ASC; return new CursorLoader( requireContext(), ContactsContract.CommonDataKinds.Phone.CONTENT_URI, projection, selection, null, sortOrder ); } Override public void onLoadFinished(NonNull LoaderCursor loader, Cursor data) { if (loader.getId() LOADER_ID_CONTACTS) { adapter.swapCursor(data); } } Override public void onLoaderReset(NonNull LoaderCursor loader) { if (loader.getId() LOADER_ID_CONTACTS) { adapter.swapCursor(null); } } }CursorLoader构造参数顺序是Context、Uri、projection、selection、selectionArgs、sortOrder。这六个参数里最容易写错的是selectionArgs如果selection里用了?占位符selectionArgs必须按顺序传值否则查询会返回空游标。3.2 AsyncLoader 子类需要覆写的方法CursorLoader本身已经实现了完整功能但如果你要自定义AsyncLoader子类必须覆写这几个方法才能形成闭环public class CustomAsyncLoader extends AsyncLoaderCursor { Override public Cursor loadInBackground() { // 后台线程执行不能碰 UI return queryContacts(); } Override protected void onStartLoading() { forceLoad(); // 触发 loadInBackground } Override protected void onStopLoading() { cancelLoad(); } Override protected void onReset() { super.onReset(); onStopLoading(); } Override public void onCanceled(Cursor data) { if (data ! null !data.isClosed()) { data.close(); } } Override protected void deliverResult(Cursor data) { if (isReset()) { if (data ! null) data.close(); return; } super.deliverResult(data); } }loadInBackground()是必须实现的抽象方法其余几个方法决定 Loader 的生命周期行为。onStartLoading()里调forceLoad()是最常见的触发方式不调的话loadInBackground()不会执行。3.3 类图字段与关系标注类图不用画得太复杂抓住继承和持有关系即可类类型关键字段/方法关系Fragment界面类getLoaderManager()持有 LoaderManagerLoaderManager管理类initLoader()、destroyLoader()管理多个 LoaderLoaderCallbacks接口onCreateLoader、onLoadFinished、onLoaderReset由 Fragment 实现AsyncLoader抽象类loadInBackground()、deliverResult()继承 LoaderCursorLoader具体类构造参数六元组继承 AsyncLoaderContactsContract数据契约CONTENT_URI、DISPLAY_NAME被 CursorLoader 查询关系标注Fragment实现LoaderCallbacksLoaderManager回调FragmentCursorLoader继承AsyncLoaderAsyncLoader继承Loader。画图时把LoaderManager放在中间左边连Fragment右边连Loader家族继承线用空心三角持有线用实心菱形。4. 验证请求确认查询回调是否触发代码写完怎么确认onCreateLoader和onLoadFinished真的被调用了按下面步骤操作第一步在三个回调里各加一行日志Override public LoaderCursor onCreateLoader(int id, Bundle args) { Log.d(LoaderTrace, onCreateLoader id id); // ... 构造 CursorLoader } Override public void onLoadFinished(LoaderCursor loader, Cursor data) { Log.d(LoaderTrace, onLoadFinished count (data null ? -1 : data.getCount())); adapter.swapCursor(data); } Override public void onLoaderReset(LoaderCursor loader) { Log.d(LoaderTrace, onLoaderReset); adapter.swapCursor(null); }第二步运行 App进入联系人列表页过滤 Logcatadb logcat -s LoaderTrace预期输出顺序是onCreateLoader id1001随后onLoadFinished countN。如果只看到onCreateLoader没有onLoadFinished说明loadInBackground()没执行或查询抛异常。第三步验证数据变化时回调是否重新触发。在联系人 App 里新增一个联系人回到你的页面观察onLoadFinished是否再次打印且count增加。CursorLoader会监听CONTENT_URI的变化数据变动时自动重新查询。第四步验证onLoaderReset。调用getLoaderManager().destroyLoader(LOADER_ID_CONTACTS)日志应出现onLoaderReset此时适配器里的游标应被置空避免使用已关闭的 Cursor。5. 本篇常见错排查5.1 回调不触发initLoader 调用时机太早如果在onCreate()里调initLoader()此时RecyclerView还没创建onLoadFinished可能先于视图初始化执行导致adapter为 null。放到onActivityCreated()或onViewCreated()里更稳。5.2 Cursor 已关闭onLoadFinished 里做了异步操作onLoadFinished返回的Cursor由 Loader 管理一旦onLoaderReset触发就会被关闭。如果你在onLoadFinished里开子线程延迟读取 Cursor很容易遇到StaleDataException。正确做法是在回调里同步把数据交给适配器或者拷贝出需要的数据。5.3 查询返回空selection 和 selectionArgs 不匹配// 错误写法selection 有 ? 但 selectionArgs 传 null new CursorLoader(ctx, uri, projection, Phone.DISPLAY_NAME ?, null, sortOrder); // 正确写法 new CursorLoader(ctx, uri, projection, Phone.DISPLAY_NAME ?, new String[]{张三}, sortOrder);5.4 权限问题READ_CONTACTS 未授权Android 6.0 以上需要运行时申请READ_CONTACTS。没授权时CursorLoader查询会返回空游标或抛SecurityException但日志里不一定明显。先在onLoadFinished里打印data.getCount()如果是 0 且确认有联系人优先检查权限。uses-permission android:nameandroid.permission.READ_CONTACTS /5.5 Loader ID 冲突多个 Loader 共用同一 ID一个 Fragment 里如果有多个查询比如全部联系人和搜索联系人必须用不同LOADER_ID。initLoader相同 ID 会复用已有 Loader不会重新创建导致onCreateLoader不执行。6. 接入与排障入口Loader 回调链路排查清楚后如果你想把联系人查询逻辑接到模型辅助的编码流程里可以从 API Keys 和接入文档开始API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite模型对话https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite长期做 Android 编码或 Agent 工具链的话Coding Plan 入口在这里https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite最后留一个实用技巧类图画完后把Fragment、LoaderManager、AsyncLoader、CursorLoader四个类的关键方法各写一行注释贴在代码旁边比反复翻图快。回调顺序记不住的时候直接在onCreateLoader、onLoadFinished、onLoaderReset三处打日志跑一遍比看任何文档都直观。
返回列表