在Activity或Fragment中加載數據,一般的I/O或數據庫操作或數據解析操作,可能會比較耗時,爲了提升用戶體驗,一般將這一的操作放在單獨的線程中運行,這樣保證操作的流暢性。
Android 3.0 中引入了加載器,支持輕鬆在 Activity 或片段中異步加載數據。 加載器具有以下特徵:
- 可用於每個 Activity 和 Fragment。
- 支持異步加載數據。
- 監控其數據源並在內容變化時傳遞新結果。
- 在某一配置更改後重建加載器時,會自動重新連接上一個加載器的 Cursor。 因此,它們無需重新查詢其數據。
使用Loader
使用加載器的應用通常包括:
- Activity 或 Fragment。
- LoaderManager 的實例。
- 一個 CursorLoader,用於加載由 ContentProvider 支持的數據。您也可以實現自己的 Loader 或 AsyncTaskLoader 子類,- 從其他源中加載數據。
- 一個 LoaderManager.LoaderCallbacks 實現。您可以使用它來創建新加載器,並管理對現有加載器的引用。
- 一種顯示加載器數據的方法,如 SimpleCursorAdapter。
- 使用 CursorLoader 時的數據源,如 ContentProvider
啓動Loader
LoaderManager 可在 Activity 或 Fragment 內管理一個或多個 Loader 實例。每個 Activity 或片段只有一個 LoaderManager。
通常,會使用 Activity 的 onCreate() 方法或片段的 onActivityCreated() 方法初始化 Loader。操作如下:
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
注意,initLoader() 方法將返回已創建的 Loader,但您不必捕獲其引用。LoaderManager 將自動管理加載器的生命週期。LoaderManager 將根據需要啓動和停止加載,並維護加載器的狀態及其相關內容。 這意味着你會很少直接與加載器進行交互
重啓Loader
當您使用 initLoader() 時(如上所述),它將使用含有指定 ID 的現有加載器(如有)。如果沒有,則它會創建一個。但有時,您想放棄這些舊數據並重新開始。
要放棄舊數據,請使用 restartLoader()。例如,當用戶的查詢更改時,此 SearchView.OnQueryTextListener 實現將重啓加載器。 加載器需要重啓,以便它能夠使用修訂後的搜索篩選器執行新查詢。
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;
}
使用 LoaderManager 回調
- onCreateLoader():針對指定的 ID 進行實例化並返回新的 Loader
當您嘗試訪問加載器時(例如,通過 initLoader()),該方法將檢查是否已存在由該 ID 指定的加載器。如果沒有,它將觸發 LoaderManager.LoaderCallbacks 方法 onCreateLoader()。在此方法中,您可以創建新加載器。 通常,這將是 CursorLoader,但您也可以實現自己的 Loader 子類。 - onLoadFinished() :將在先前創建的加載器完成加載時調用
當先前創建的加載器完成加載時,將調用此方法。該方法必須在爲此加載器提供的最後一個數據釋放之前調用。 此時,您應移除所有使用的舊數據(因爲它們很快會被釋放),但不要自行釋放這些數據,因爲這些數據歸其加載器所有,其加載器會處理它們。
當加載器發現應用不再使用這些數據時,即會釋放它們。 例如,如果數據是來自 CursorLoader 的一個 Cursor,則您不應手動對其調用 close()。如果 Cursor 放置在 CursorAdapter 中,則應使用 swapCursor() 方法,使舊 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);
}
- onLoaderReset(): 將在先前創建的加載器重置且其數據因此不可用時調用
此方法將在先前創建的加載器重置且其數據因此不可用時調用。 通過此回調,您可以瞭解何時將釋放數據,因而能夠及時移除其引用。
此實現調用值爲 null 的swapCursor():
// 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,其中包含針對聯繫人內容提供程序的查詢結果。它使用 CursorLoader 管理提供程序的查詢。
應用如需訪問用戶聯繫人(正如此示例中所示),其清單文件必須包括權限 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);
}
}
更多示例
加載設備中所有安裝應用的例子,使用自定義Loader的方式實現:
public static class AppListLoader extends AsyncTaskLoader<List<AppEntry>> {
final InterestingConfigChanges mLastConfig = new InterestingConfigChanges();
final PackageManager mPm;
List<AppEntry> mApps;
PackageIntentReceiver mPackageObserver;
public AppListLoader(Context context) {
super(context);
// Retrieve the package manager for later use; note we don't
// use 'context' directly but instead the save global application
// context returned by getContext().
mPm = getContext().getPackageManager();
}
/**
* This is where the bulk of our work is done. This function is
* called in a background thread and should generate a new set of
* data to be published by the loader.
*/
@Override public List<AppEntry> loadInBackground() {
// Retrieve all known applications.
List<ApplicationInfo> apps = mPm.getInstalledApplications(
PackageManager.GET_UNINSTALLED_PACKAGES |
PackageManager.GET_DISABLED_COMPONENTS);
if (apps == null) {
apps = new ArrayList<ApplicationInfo>();
}
final Context context = getContext();
// Create corresponding array of entries and load their labels.
List<AppEntry> entries = new ArrayList<AppEntry>(apps.size());
for (int i=0; i<apps.size(); i++) {
AppEntry entry = new AppEntry(this, apps.get(i));
entry.loadLabel(context);
entries.add(entry);
}
// Sort the list.
Collections.sort(entries, ALPHA_COMPARATOR);
// Done!
return entries;
}
/**
* Called when there is new data to deliver to the client. The
* super class will take care of delivering it; the implementation
* here just adds a little more logic.
*/
@Override public void deliverResult(List<AppEntry> apps) {
if (isReset()) {
// An async query came in while the loader is stopped. We
// don't need the result.
if (apps != null) {
onReleaseResources(apps);
}
}
List<AppEntry> oldApps = apps;
mApps = apps;
if (isStarted()) {
// If the Loader is currently started, we can immediately
// deliver its results.
super.deliverResult(apps);
}
// At this point we can release the resources associated with
// 'oldApps' if needed; now that the new result is delivered we
// know that it is no longer in use.
if (oldApps != null) {
onReleaseResources(oldApps);
}
}
/**
* Handles a request to start the Loader.
*/
@Override protected void onStartLoading() {
if (mApps != null) {
// If we currently have a result available, deliver it
// immediately.
deliverResult(mApps);
}
// Start watching for changes in the app data.
if (mPackageObserver == null) {
mPackageObserver = new PackageIntentReceiver(this);
}
// Has something interesting in the configuration changed since we
// last built the app list?
boolean configChange = mLastConfig.applyNewConfig(getContext().getResources());
if (takeContentChanged() || mApps == null || configChange) {
// If the data has changed since the last time it was loaded
// or is not currently available, start a load.
forceLoad();
}
}
/**
* Handles a request to stop the Loader.
*/
@Override protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
/**
* Handles a request to cancel a load.
*/
@Override public void onCanceled(List<AppEntry> apps) {
super.onCanceled(apps);
// At this point we can release the resources associated with 'apps'
// if needed.
onReleaseResources(apps);
}
/**
* Handles a request to completely reset the Loader.
*/
@Override protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
// At this point we can release the resources associated with 'apps'
// if needed.
if (mApps != null) {
onReleaseResources(mApps);
mApps = null;
}
// Stop monitoring for changes.
if (mPackageObserver != null) {
getContext().unregisterReceiver(mPackageObserver);
mPackageObserver = null;
}
}
/**
* Helper function to take care of releasing resources associated
* with an actively loaded data set.
*/
protected void onReleaseResources(List<AppEntry> apps) {
// For a simple List<> there is nothing to do. For something
// like a Cursor, we would close it here.
}
}
核心實現在Fragment中:
public static class AppListFragment extends ListFragment
implements OnQueryTextListener, LoaderManager.LoaderCallbacks<List<AppEntry>> {
// This is the Adapter being used to display the list's data.
AppListAdapter 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 applications");
// 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 AppListAdapter(getActivity());
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);
}
@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
| MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
SearchView sv = new SearchView(getActivity());
sv.setOnQueryTextListener(this);
item.setActionView(sv);
}
@Override public boolean onQueryTextChange(String newText) {
// Called when the action bar search text has changed. Since this
// is a simple array adapter, we can just have it do the filtering.
mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
mAdapter.getFilter().filter(mCurFilter);
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("LoaderCustom", "Item clicked: " + id);
}
@Override public Loader<List<AppEntry>> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader with no arguments, so it is simple.
return new AppListLoader(getActivity());
}
@Override public void onLoadFinished(Loader<List<AppEntry>> loader, List<AppEntry> data) {
// Set the new data in the adapter.
mAdapter.setData(data);
// The list should now be shown.
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
@Override public void onLoaderReset(Loader<List<AppEntry>> loader) {
// Clear the data in the adapter.
mAdapter.setData(null);
}
}
列表適配器:
public static class AppListAdapter extends ArrayAdapter<AppEntry> {
private final LayoutInflater mInflater;
public AppListAdapter(Context context) {
super(context, android.R.layout.simple_list_item_2);
mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public void setData(List<AppEntry> data) {
clear();
if (data != null) {
addAll(data);
}
}
/**
* Populate new items in the list.
*/
@Override public View getView(int position, View convertView, ViewGroup parent) {
View view;
if (convertView == null) {
view = mInflater.inflate(R.layout.list_item_icon_text, parent, false);
} else {
view = convertView;
}
AppEntry item = getItem(position);
((ImageView)view.findViewById(R.id.icon)).setImageDrawable(item.getIcon());
((TextView)view.findViewById(R.id.text)).setText(item.getLabel());
return view;
}
}
參考地址:http://developer.android.com/intl/zh-cn/guide/components/loaders.html