CursorLoader为ListFragment中ListView加载数据的流程

本文详细解析了Android中Loader机制的工作原理,特别是LoaderManager、CursorLoader及ContentProvider之间的交互方式,帮助开发者理解如何利用Loader进行高效的数据加载和更新。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天学习了下ApiDemo中的LoaderThrottle示例,内容涉及到LoaderManager、CursorLoader、ContentProvider等几个重要类,值得个人学习。

现将个人的学习心得分享出来,其中难免有理解上的误区,希望大家多多提出意见。


Android 3.0之后引入了Loaders类,为Activity和Fragment提供了一种很好的异步数据加载机制,详细的介绍大家可用参考官方文档进一步学习。

Activity和Fragment通过LoaderManager来对Loaders进行管理,涉及到的主要类和接口包含LoaderManager、LoaderManager.LoaderCallbacks、Loader、AsyncTaskLoader和CursorLoader。其中CursorLoader继承自AsyncTaskLoader,而AsyncTaskLoader是Loader的直接抽象子类。

我们知道,ListFragment中内嵌了一个ListView,我们可用直接调用ListFragment#setListAdapter方法来绑定Adapter。现在我们来看Demo中的主要代码:

1、自定义一个ListFragment,并实现LoaderManager.LoaderCallbacks接口:

public static class ThrottledLoaderListFragment extends ListFragment
            implements LoaderManager.LoaderCallbacks<Cursor> {
//代码参考下文
}

在这个类中,我们主要在onActivityCreated方法中进行核心代码的初始化:

 setHasOptionsMenu(true);

            // Create an empty adapter we will use to display the loaded data.
            mAdapter = new SimpleCursorAdapter(getActivity(),
                    android.R.layout.simple_list_item_1, null,
                    new String[] { MainTable.COLUMN_NAME_DATA },
                    new int[] { android.R.id.text1 }, 0);
            setListAdapter(mAdapter);

            // Start out with a progress indicator.
            setListShown(false);

            // Prepare the loader.  Either re-connect with an existing one,
            // or start a new one.
            getLoaderManager().initLoader(0, null, this);

注意上面代码片段中最后一行代码
 getLoaderManager().initLoader(0, null, this);
这行代码可用确保对Loader进行初始化并将其激活。在初始化的时候,会调用LoaderManager.LoaderCallbacks的两个主要方法:

 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            CursorLoader cl = new CursorLoader(getActivity(), MainTable.CONTENT_URI,
                    PROJECTION, null, null, null);
            cl.setUpdateThrottle(2000); // update at most every 2 seconds.
            return cl;
        }

 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
            mAdapter.swapCursor(data);

            // The list should now be shown.
            if (isResumed()) {
                setListShown(true);
            } else {
                setListShownNoAnimation(true);
            }
        }

在调用onCreateLoader方法创建Loader'成功后,当Loader数据加载完成,会自动调用onLoadFinished方法。在这个方法中,核心代码是

 mAdapter.swapCursor(data);
这行代码其实就是CursorLoader与ListFragment通信的关键。我们来看其实现:

 @Override
    public Cursor swapCursor(Cursor c) {
        Cursor res = super.swapCursor(c);
        // rescan columns in case cursor layout is different
        findColumns(mOriginalFrom);
        return res;
    }

这个方法实现很简单,关键部分是super.swapCursor(c)方法的调用:

public Cursor swapCursor(Cursor newCursor) {
        if (newCursor == mCursor) {
            return null;
        }
        Cursor oldCursor = mCursor;
        if (oldCursor != null) {
            if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
            if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
        }
        mCursor = newCursor;
        if (newCursor != null) {
            if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
            if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
            mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
            mDataValid = true;
            // notify the observers about the new cursor
            notifyDataSetChanged();
        } else {
            mRowIDColumn = -1;
            mDataValid = false;
            // notify the observers about the lack of a data set
            notifyDataSetInvalidated();
        }
        return oldCursor;
    }

针对Demo的实现,我们主要关心的还是这行代码:

// notify the observers about the new cursor
            notifyDataSetChanged();
我们知道,这个方法其实就是CursorAdapter通知ListView对数据进行刷新。

到这里,我们就应该知道了CursorLoader是如何通知ListFragment来刷新界面的了。

2、现在还有一个问题需要说明下,那就是在Demo中,当向ContentProvider中添加数据时,CursorLoader是如何知道数据发生了变化并通知ListView刷新界面的呢?

在菜单点击动作中,有这样一段代码:

final ContentResolver cr = getActivity().getContentResolver();

//此处省略部分代码

case POPULATE_ID:
                    if (mPopulatingTask != null) {
                        mPopulatingTask.cancel(false);
                    }
                    mPopulatingTask = new AsyncTask<Void, Void, Void>() {
                        @Override protected Void doInBackground(Void... params) {
                            for (char c='Z'; c>='A'; c--) {
                                if (isCancelled()) {
                                    break;
                                }
                                StringBuilder builder = new StringBuilder("Data ");
                                builder.append(c);
                                ContentValues values = new ContentValues();
                                values.put(MainTable.COLUMN_NAME_DATA, builder.toString());
                                cr.insert(MainTable.CONTENT_URI, values);
                                // Wait a bit between each insert.
                                try {
                                    Thread.sleep(250);
                                } catch (InterruptedException e) {
                                }
                            }
                            return null;
                        }
                    };
                    mPopulatingTask.executeOnExecutor(
                            AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
                    return true;

我们注意到,在这段代码中,程序调用了ContentResolver的insert方法。该方法其实是对ContentProvider中insert方法的调用。我们来看下Demo中SimpleProvider(继承自ContentProvider)中insert方法(部分):

 SQLiteDatabase db = mOpenHelper.getWritableDatabase();

            long rowId = db.insert(MainTable.TABLE_NAME, null, values);

            // If the insert succeeded, the row ID exists.
            if (rowId > 0) {
                Uri noteUri = ContentUris.withAppendedId(MainTable.CONTENT_ID_URI_BASE, rowId);
                getContext().getContentResolver().notifyChange(noteUri, null);
                return noteUri;
            }

上面代码中,我们注意这行代码:

getContext().getContentResolver().notifyChange(noteUri, null);
我们查看文档中对notifyChange介绍就知道,它实际上是告诉ContentObserver(第二个参数)对应的URI(第一个参数)发生了变化。然而,在这里,ContentObserver却为null。那么,当URI发生变化,ListFragment又是如何知道的呢?

其实,在Demo中,SimpleProvider的query方法已经告诉了我们:

@Override
		public Cursor query(Uri uri, String[] projection, String selection,
				String[] selectionArgs, String sortOrder) {
			
			SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
			queryBuilder.setTables(MainTable.TABLE_NAME);
			
			switch(uriMatcher.match(uri)) {
			case MAIN: queryBuilder.setProjectionMap(mainProjectionMap);break;
			case MAIN_ID: 
				queryBuilder.setProjectionMap(mainProjectionMap);
				queryBuilder.appendWhere(MainTable._ID + " =?");
				selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[]{uri.getLastPathSegment()});
				break;
			
			}
			if(TextUtils.isEmpty(sortOrder)){
				sortOrder = MainTable.DEFAULT_SORT_ORDER;
			}
			
			SQLiteDatabase db = dbHelper.getWritableDatabase();
			Cursor c = queryBuilder.query(db, projection, selection, selectionArgs,
                    null /* no group */, null /* no filter */, sortOrder);
			c.setNotificationUri(getContext().getContentResolver(), uri);
			return c;
		}

倒数第二行代码已经很明显了:

c.setNotificationUri(getContext().getContentResolver(), uri);
这行代码告诉ContentResolver上的监听器,当URI变化时要做相应的更新。

3、大家可能觉得调用过程已经很清晰了,可是,你搜遍整个Demo代码,你会发现,没有找到对SimpleProvider中query方法的调用。既然这样,上面的代码片段岂不是根本就不可能执行吗,又怎么能监听到数据变化呢?

其实,我们忽略了一个地方,那就是LoaderManager。通过断点调试,我们可用看到,实际上LoaderManager调用了CursorLoader,而CursorLoader调用了这里的query方法:


由于本人菜鸟一枚,关于应用如果管理LoaderManager,LoaderManager是在什么时候调用了CursorLoader,我也没搞懂,有知道的朋友欢迎留言指出,在此感激不尽。


总结:本人的表达很是欠佳,可能很多地方说的不够周密。在此,对本文作个简单总结,希望大家能够不吝赐教。

LoaderManager调用了CursorLoader,CursorLoader在初始化成功后,会调用绑定的ContentProvider的query方法,进而对URI的变化注册监听器。当URI变化时,CursorLoader会调用CursorAdapter的sawpCursor方法,从而通知ListFragment更新UI界面。



PS:LoaderThrottle中涉及到SQLiteDatabase、ContentProvider、Loader的使用,强烈建议大家去研究研究。源码路径:导入ApiDemo后,在com.example.android.apis.app包下。


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值