原文地址:Loaders
一、说明
loader从Android 3.0开始引入。loader的作用是简化fragment或activity中异步加载数据的过程。loader有以下特征:
- loader在每个activity与fragment都可用
- loader提供数据的异步加载功能
- loader会监听源数据,在内容变化时发送新数据
- 当配置改变后重启时,loader会自动重连到最后loader的cursor,因此loader不需要重新查询数据
二、loader API简介
Class/Interface | 描述 |
---|---|
LoaderManager | 抽象类,Activity或Fragment可以用它管理一个或多个loader。它可以让应用结合activity或fragment的生命周期管理长连接操作,最常见的用法是使用CursorLoader。应用也可以编写自己的loader来加载其他类型的数据。 每个activity或fragment只能有一个LoaderManager,一个LoaderManager可以有多个loader |
LoaderManager.LoaderCallbacks | 使用方与LoaderManger交互的回调接口。例如,你可以使用onCreateLoader()回调创建一个新的loader |
Loader | 抽象类,可以执行异步数据加载。它是loader的基类。你可以使用CurrorLoader,也可以自己继承Loader类编写loader。自定义的loader开始工作时,应该监控数据源变化,并在content发生变化时分发新的结果 |
AsyncTaskLoader | 提供AsyncTask的抽象loader |
CursorLoader | AsyncTaskLoader的子类,会查询ContentResolver并返回Cursor。此类是查询cursor的标准实现,它会构建一个AsyncTaskLoader,其会在后台线程查询cursor,不会阻塞UI线程。使用此loader是从ContentProvider异步加载数据的最好方式,请尽量避免直接用activity或fragment的API进行查询 |
以上的类和接口是你实现一个loader必要的成分。当你创建loader时,你不需要以上的所有内容,但是LoaderManager(其会初始化loader)和Loader实例(继承了Loader类的loader,比如CursorLoader)是必须的。下面的部分描述了怎样使用这些类和接口。
三、在应用中使用loader
应用使用loader一般包括以下步骤:
- 有activity或fragment
- 创建LoaderManager实例
- 创建CursorLoader加载ContentProvider的数据。你也可以实现自己的loader或AsyncTaskLoader去加载其他种类的数据
- 实现LoaderManager.LoaderCallbacks。从这个回调中,你可以创建新的loader,或者管理已经存在的loader
- 用某种方式显示loader的数据,如SimpleCursorAdapter
- 指定数据源,如ContentProvider
3.1 启动loader
LoaderManager可以在activity或fragment中管理一个或多个loader。每个fragment或activity只能有一个LoaderManager。
通常,你可以在activity的onCreate()方法或者fragment的onActivityCreated()方法初始化loader。初始化代码如下:
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
initLoader()方法的参数依次如下:
- 标识loader的唯一的ID。本例中为0
- 添加到loader的可选参数。本例中为null
- LoaderManager.LoaderCallbacks实例。LoaderManager会通过此回调报告loader的事件。在本例中,此类实现了LoaderManager.LoaderCallbacks接口,所以将它自己传入initLoader中
initLoader()方法会实例化loader并激活它。此方法逻辑如下:
- 如果为loader指定的ID已存在,那么会直接重用存在的loader
- 如果为loader指定的ID不存在,那么initLoader()方法会调用LoaderManager.LoaderCallbacks的onCreateLoader()方法,在这个方法里,你可以实例化并返回一个新的loader
当loader状态改变时,LoaderManager.LoaderCallbacks会收到回调。如果调用initLoader方法时调用方在started状态,而且指定的loader已经存在且生成了它的数据,那么系统会立即调用onLoadFinished()方法(在initLoader()期间),你必须处理这种可能性。
initLoader()方法会返回一个它创建的Loader,但是你不必持有此loader的引用。LoaderManager会自动管理loader的生命周期。LoaderManager会在必要时启动或者停止加载、维持loader状态、维持它连接的内容。从这个角度讲,你很少会直接与loader进行交互。你更多的是通过LoaderManager.LoaderCallbacks去干涉加载过程。
3.2 重启loader
当你使用initLoader()方法时,如果指定ID的loader已经存在,那么会直接重用。如果指定ID的loader不存在,那么会创建。有时,可能你想要抛弃旧的数据并重新查询。
要抛弃旧数据,你可以使用restartLoader()方法。在下面的例子中,SearchView.OnQueryTextListener会在用户查询条件改变时重启loader。然后loader可以按照新的条件加载数据。
public boolean onQueryTextChanged(String newText) {
// Called when the action bar search text has changed. Update
// the search filter, and restart the loader to do a new query
// with this filter.
mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
getLoaderManager().restartLoader(0, null, this);
return true;
}
3.3 使用LoaderManager回调
使用方可以通过LoaderManager.LoaderCallbacks接口与LoaderManager交互。
loader,如CursorLoader,可以在被停止时保存数据,这样的话,应用就可以在activity或fragment在onStop()并onStart()后保持数据,当用户返回到应用时,就不必等待数据重新加载了。使用LoaderManager.LoaderCallbacks可以知道什么时候创建了新的loader,并通知应用什么时候停止使用loader的数据。
LoaderManager.LoaderCallbacks包括以下方法:
- onCreateLoader()。根据给定ID实例化并返回一个新的loader
- onLoadFinished()。当之前创建的loader结束加载时调用
- onLoaderReset()。当之前创建的loader被重置时调用,在这个方法中,你应该使你的数据不可见
这些方法的详细信息如下:
3.3.1 onCreateLoader
当你试图访问一个loader时(如使用initLoader()方法),会检查指定ID的loader是否存在。如果不存在,会触发LoaderManager.LoaderCallbacks的onCreateLoader()方法。在这里,你应该创建一个新的loader。
在下面的例子中,onCreaeteLoader()会创建一个CursorLoader,CursorLoader的构造函数的参数包括:
- uri。搜索的URI
- projection。表示返回哪些列。传入null会返回所有列,可能会影响效率
- selection。表示返回哪些行。传入null将返回指定URI的所有行
- selectionArgs。占位符的数据
- sortOrder。表示返回结果的排序方式。传入null将使用默认的排序方式
// If non-null, this is the current filter the user has provided.
String mCurFilter;
...
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader, so we don't care about the ID.
// First, pick the base URI to use depending on whether we are
// currently filtering.
Uri baseUri;
if (mCurFilter != null) {
baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
Uri.encode(mCurFilter));
} else {
baseUri = Contacts.CONTENT_URI;
}
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
+ Contacts.HAS_PHONE_NUMBER + "=1) AND ("
+ Contacts.DISPLAY_NAME + " != '' ))";
return new CursorLoader(getActivity(), baseUri,
CONTACTS_SUMMARY_PROJECTION, select, null,
Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
}
3.3.2 onLoadFinished
当之前创建的loader结束加载时调用此方法。这个方法是在loader提供的最后数据释放前调用的。在这个方法中,你应该移除对旧数据的使用,但是不要关闭数据源,loader会自己处理它。
loader会在知道应用不再使用它后释放这些数据。例如,如果数据是CursorLoader的光标,你不应该调用它的cloase()方法,如果CursorAdapter的cursor被更换了,你应该使用swapCursor()方法更新cursor,这样的话旧的Cursor会被关闭。示例代码如下:
// This is the Adapter being used to display the list's data.
SimpleCursorAdapter mAdapter;
...
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
}
3.3.3 onLoaderReset
当之前创建的loader被重置时调用此方法,会使loader的数据不可见。这个回调可以提示你数据快要被释放了,这样你就可以移除对数据的引用。
下面的示例中调用swapCursor()方法并传入了null:
// This is the Adapter being used to display the list's data.
SimpleCursorAdapter mAdapter;
...
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
mAdapter.swapCursor(null);
}
四、示例
下面是Fragment用listview显示content provider查询结果的示例。它使用了CursorLoader管理对content provider的查询。
应用要访问用户的联系人,需要在manifest中声明READ_CONTACTS权限。
public static class CursorLoaderListFragment extends ListFragment
implements OnQueryTextListener, LoaderManager.LoaderCallbacks<Cursor> {
// This is the Adapter being used to display the list's data.
SimpleCursorAdapter mAdapter;
// If non-null, this is the current filter the user has provided.
String mCurFilter;
@Override public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Give some text to display if there is no data. In a real
// application this would come from a resource.
setEmptyText("No phone numbers");
// We have a menu item to show in action bar.
setHasOptionsMenu(true);
// Create an empty adapter we will use to display the loaded data.
mAdapter = new SimpleCursorAdapter(getActivity(),
android.R.layout.simple_list_item_2, null,
new String[] { Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS },
new int[] { android.R.id.text1, android.R.id.text2 }, 0);
setListAdapter(mAdapter);
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
}
@Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// Place an action bar item for searching.
MenuItem item = menu.add("Search");
item.setIcon(android.R.drawable.ic_menu_search);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
SearchView sv = new SearchView(getActivity());
sv.setOnQueryTextListener(this);
item.setActionView(sv);
}
public boolean onQueryTextChange(String newText) {
// Called when the action bar search text has changed. Update
// the search filter, and restart the loader to do a new query
// with this filter.
mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
getLoaderManager().restartLoader(0, null, this);
return true;
}
@Override public boolean onQueryTextSubmit(String query) {
// Don't care about this.
return true;
}
@Override public void onListItemClick(ListView l, View v, int position, long id) {
// Insert desired behavior here.
Log.i("FragmentComplexList", "Item clicked: " + id);
}
// These are the Contacts rows that we will retrieve.
static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
Contacts._ID,
Contacts.DISPLAY_NAME,
Contacts.CONTACT_STATUS,
Contacts.CONTACT_PRESENCE,
Contacts.PHOTO_ID,
Contacts.LOOKUP_KEY,
};
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader, so we don't care about the ID.
// First, pick the base URI to use depending on whether we are
// currently filtering.
Uri baseUri;
if (mCurFilter != null) {
baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
Uri.encode(mCurFilter));
} else {
baseUri = Contacts.CONTENT_URI;
}
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
+ Contacts.HAS_PHONE_NUMBER + "=1) AND ("
+ Contacts.DISPLAY_NAME + " != '' ))";
return new CursorLoader(getActivity(), baseUri,
CONTACTS_SUMMARY_PROJECTION, select, null,
Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
}
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
}
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
mAdapter.swapCursor(null);
}
}