1.2.2 Loaders - 加载器
Loaders从Android 3.0引入,它使得在activity或是fragment里进行异步数据加载变得非常简单。Loaders有如下的特性:
- 它在每个 Activity 和 Fragment 里都可用。
- 它提供了异步读取数据的机制。
- 它可以监控数据相关的资源,当它们的内容发生变化时,会分发新的结果。
- 在配置发生变化引起重建后,它们会自动重新连接到最新loader的游标上。因此,不需要重新查询数据。
Loaders API Summary - 加载器API摘要
在应用里,Loaders提供了很多的类和接口,表格里是类的概要说明:
Class/Interface |
Description--描述 |
与 Activity 和 Fragment 相关联的抽象类,用于管理一个或多个 Loader 实例。结合Activity 或 Fragment的生命周期,它有助于应用管理长时间运行的操作。最常见的使用是 CursorLoader,但是,也可以写自己的加载器来读取其它类型的数据。 在每个activity或fragment里,仅仅只有一个LoaderManager,但是一个LoaderManager可以管理多个Loader。 |
|
与LoaderManager联系的客户端程序的回调接口。例如,你可以使用onCreateLoader() 回调函数来创建一个新的Loader。 |
|
实现了异步读取数据的抽象类。它是Loader的基类。默认的,你应该实现 CursorLoader,但是你也可以实现你自己的子类。当Loaders处于激活的状态,那么它就会监听它们数据的变化,并且当内容发生变化时,会分发新的结果。 |
|
抽象的loader,提供了 AsyncTask 。 |
|
AsyncTaskLoader 的子类,它会查询 ContentResolver 并返回一个 Cursor。在查询游标时,该类以标准的方式实现了 Loader 接口,在后台,它基于AsyncTaskLoader 来执行一个游标查询,因此,它不会锁住应用的UI。从 ContentProvider里异步读取数据,使用loader是最好的方法,而不是执行一个受fragmentation或activity的API管理的查询。 |
上面表格里展示的类和接口是最基本的组件,在应用时你可能会使用它们来实现一个loader。你创建的每个loader不一定都会用到它们的所有,但是你为了实例化一个loader,你必须需要一个LoaderManager的引用和Loader类的实现(例如CursorLoader)。下面的内容描述了在应用里如何使用类和接口。
Using Loaders in an Application - 在应用里使用Loader
本章节描述了如何在Android应用里使用loader。一般来说,使用loader的应用包含如下要素:
- 一个 Activity 或 Fragment 。
- 一个LoaderManager的实例。
- 由ContentProvider返回的用来读取数据的CursorLoader。你也可以实现一个Loader或AsyncTaskLoader的子类来从一些其它的资源里读取数据。
- LoaderManager.LoaderCallbacks的实现。在它的实现里,你可以创建新的loader,还可以管理已经存在的loader的引用。
- 显示loader数据的方法,例如显示SimpleCursorAdapter 数据的方法。
- 数据资源,例如在使用 CursorLoader 时的 ContentProvider 。
Starting a Loader - 开启Loader
在activity或fragment里,LoaderManager管理管理一个或多个Loader的实例。在activity或fragment里,仅仅只能有一个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,在例子中,loader的id为0。
- 参数可选,用于在构造方法给loader提供(例子中为null)。
- LoaderManager.LoaderCallbacks的实现,LoaderManager调用它来发布loader的事件。在例子中,类已经实现了LoaderManager.LoaderCallbacks接口,因此给方法传递了自己的引用--this。
调用initLoader() 方法是为了确保loader被实例化和被激活,这样做可能会有两个结果:
- 如果指定了ID的loader已经存在,那么最后被创建的loader被重用。
- 如果指定了ID的loader不存在,那么,initLoader() 方法会触发LoaderManager.LoaderCallbacks的onCreateLoader() 方法。在这个方法里,你可以实现用来实例化一个新的loader,并返回它。更多详情请参见 onCreateLoader 章节。
在这两种情况下,LoaderManager.LoaderCallbacks的实现和loader紧紧的联系着,当loader的状态发生变化时,该实现就会被调用。如果在开始状态就调用它,并且要使用的loader已经存在并生成数据了,那么系统就会立即调用 onLoadFinished() 方法(在initLoader() 期间),因此你必须为这可能发生的事件做准备。 更多详情请参见onLoadFinished 章节。
注意:initLoader() 方法返回了创建的Loader,但是你不需要获取它的引用。LoaderManager会自动管理loader的生命周期。如果有需要,LoaderManager会开始和停止读取,并且维持loader和与它并联的内容的状态。基于这样的实现,你基本上不用直接作用于loader( LoaderThrottle 示例,它使用了loader的方法来调整loader的行为)。大多数时候,当特殊事件发生时,通常会使用LoaderManager.LoaderCallbacks方法来干涉读取的进程。这个专题的大多数讨论,请参见 Using the LoaderManager Callbacks 章节。
Restarting a Loader - 重启Loader
正如上面所说的那样, 当你使用initLoader()时,如果已经有与指定的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;
}
Using the LoaderManager Callbacks - 使用LoaderManager回调
LoaderManager.LoaderCallbacks回调接口可以让客户端程序与LoaderManager进行交互。
loader,尤其是CursorLoader,在停止以后仍然期望它保持着数据。也就是说,在activity或fragment的 onStop() 和onStart() 方法里允许应用保持数据,这样的话,当用户返回到activity或fragment里,他们就不必为数据的重新加载而等待。你使用LoaderManager.LoaderCallbacks方法时,知道何时创建一个新的loader,并且告诉应用,是时候停止使用loader的数据了。
LoaderManager.LoaderCallbacks包含了下面的方法:
- onCreateLoader() -- 根据ID参数实例化一个新的Loader并返回它。
- onLoadFinished() -- 当先前创建的loader已经完成了加载后调用该方法。
- onLoaderReset() -- 当先前创建的loader重置后创建它,从而使其数据不可用。
OnCreateLoader
当你试着获取一个loader时(例如,通过initLoader()方法获取),它会通过ID来检查是否拥有该ID的loader已经存在。如果不存在,它就会触发LoaderManager.LoaderCallbacks的onCreateLoader() 方法。该方法是创建一个新loader的方法。通常来说,创建的是一个CursorLoader,但是你也可以实现你自己的Loader子类。
在例子中,onCreateLoader()回调创建了一个CursorLoader。你必须使用它的构造器方法来构造CursorLoader,这需要包含完整信息的提供给ContentProvider 的查询。需要的信息通常有:
- uri - 要检索内容的URI
- projection - 要返回列的清单。如果传递null的话会返回所有的列,这样做效率是很差的。
- selection - 申明了要返回行的过滤器,格式和sql的where子句一样(不包括where本身)。传递null的话会根据所给的URI返回所有的行。
- selectionArgs - 你可能会包含多个?在selection中,在selection里的?将会被selectionArgs里的值依次按照出现的顺序所代替。这些值会被当作字符串来处理。
- sortOrder - 如何来排序结果行,格式和sql的order子句一样(不包括order本身)。传递null的话会使用默认排序,也有可能是无序的。
例子:
// 如果非空,就是用户提供的过滤器
String mCurFilter;
...
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// 当新的loader被创建时,该方法被调用
// 该例中仅仅只有一个loader,因此我们不根据ID来创建它。
// 首先,使用一个基于当前过滤器的URI。
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");
}
OnLoadFinished
当先前创建的loader完成数据加载后调用该方法。该方法要确保优先释放提供给该加载器最后的数据。这时你应该移除所有的旧数据(因为它们马上就会被释放的),但是你不要自己去释放它们,因为loader自己会处理这些的。
当loader发现应用不再使用数据时会把它们释放掉的。例如,如果数据是由CursorLoader提供的,你就不要自己去调用close() 方法。如果游标被放在CursorAdapter 里,你应该使用 swapCursor() 方法来使旧的Cursor 不被关闭。例如:
// 用来显示列表数据的Adapter
SimpleCursorAdapter mAdapter;
...
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// 用新的游标进行交换。(一旦我们返回了旧游标,框架会关闭旧游标的)
mAdapter.swapCursor(data);
}
OnLoaderReset
当先前创建的loader被重置后调用该方法,这样就会使得它的数据不可用了。该回调方法会让你知道什么时候数据将被释放,你就可以在该方法里移除引用。
实现里调用了 swapCursor()方法,传递参数null:
// 用来显示列表数据的Adapter
SimpleCursorAdapter mAdapter;
...
public void onLoaderReset(Loader<Cursor> loader) {
// 当提供给onLoadFinished()的Cursor即将被关闭时调用该方法。我们要做的是确保不再使用它。
mAdapter.swapCursor(null);
}
Example - 示例
在下面的例子中,有Fragment完整实现,在Fragment里,用一个ListView 来显示从联系人内容提供器里获取的数据。这使用CursorLoader来管理基于提供器的查询。
要像下面的例子一样来获取用户的联系人,应用的mainfest文件里必须包含READ_CONTACTS 权限。
public static class CursorLoaderListFragment extends ListFragment
implements OnQueryTextListener, LoaderManager.LoaderCallbacks<Cursor> {
// 用于展示列表数据的适配器
SimpleCursorAdapter mAdapter;
// 如果不为空,就是当前用户提供的过滤器
String mCurFilter;
@Override public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// 如果没有数据,显示一些字符。
// 在真正的应用中,该字符应该来源于资源文件。
setEmptyText("No phone numbers");
// 在action bar里显示菜单项
setHasOptionsMenu(true);
// 创建一个空的adapter用来显示加载的数据
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);
// 准备loader。如果loader已经存在,重新连接;如果不存在,创建一个新的
getLoaderManager().initLoader(0, null, this);
}
@Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// 放置一个action bar元素用于搜索
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) {
// 当action bar的搜索文本改变时调用该方法。
// 更新查询过滤器,并重启loader来执行一个基于新过滤器的查询
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) {
// 在这里打印行为描述的日志
Log.i("FragmentComplexList", "Item clicked: " + id);
}
// 下面是要检索的联系人行
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) {
// 当需要创建新的loader时调用该方法。
// 示例中仅仅只有一个loader,因此我们不关心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);
}
}
More Examples - 更多示例
在ApiDemos里有其它的示例来说明如何使用loader:
- LoaderCursor -- 上面显示的代码片断的完整版本。
- LoaderThrottle -- 当数据发生变化时,如何通过节流来控制查询内容提供者的数量。