今天学习了下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包下。