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

来源:互联网 发布:2017淘宝新玩法 编辑:程序博客网 时间:2024/06/12 22:29

今天学习了下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方法已经告诉了我们:

@Overridepublic 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包下。


0 0
原创粉丝点击