Android Loader(一) 概述
Android Loader(三) 结合CursorLoader分析Loader相关源码
CursorLoader是AsyncTaskLoader的子类,它可以异步查询ContentProvider中得数据,在ContentProvider中数据变化时,自动重新查询。
通过CursorLoader加载ContentProvider中数据有以下2步:
1. 实现LoaderManager.LoaderCallbacks
2. 重写onCreateLoader,onLoadFinished,onLoaderReset方法。
下面用一个API DEMO例子说明:
以下代码实现一个模糊查询手机中联系人,并显示在listview上的功能,由于联系人的数据系统已经通过Contacts的ContentProvider提供,访问联系人数据直接调用系统URI即可。
public class LoaderCursor extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FragmentManager fm = getFragmentManager();
if (fm.findFragmentById(android.R.id.content) == null) {
CursorLoaderListFragment list = new CursorLoaderListFragment();
fm.beginTransaction().add(android.R.id.content, list).commit();
}
}
public static class CursorLoaderListFragment extends ListFragment
implements OnQueryTextListener, LoaderManager.LoaderCallbacks {
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);
// 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);
}
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.
String newFilter = !TextUtils.isEmpty(newText) ? newText : null;
// Don't do anything if the filter hasn't actually changed.
// Prevents restarting the loader when restoring state.
if (mCurFilter == null && newFilter == null) {
return true;
}
if (mCurFilter != null && mCurFilter.equals(newFilter)) {
return true;
}
mCurFilter = newFilter;
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 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 loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
// The list should now be shown.
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
public void onLoaderReset(Loader 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);
}
}
}
在onActivityCreated方法中调用getLoaderManager().initLoader(0, null, this);初始化Loader,在 Fragment中实现了LoaderManager.LoaderCallbacksonCreateLoader以ContentProvider的URI为参数,创建并返回一个CursorLoader。 在查询完毕后,会调用 onLoadFinished回调,所以在调用SimpleCursorAdapter的swapCursor(data),将查询后的数据显示在ListView上。最后,在Loader的数据不再使用时,onLoaderReset被调用, mAdapter.swapCursor(null); 保证旧的Cursor数据不再被使用。在 onQueryTextChange方法中,调用 getLoaderManager().restartLoader(0, null, this); 会使Loader重新创建, onCreateLoader-》onLoadFinished的回调方法会被重新执行。
下面从ContentProvider实现开始,实现一个CursorLoader的完整例子:
下面的程序实现的功能:从记录城市的表CITY表中查询出城市名称,显示在ListView上。
工程目录:
先定义表结构:
CityTable.java:
public class CityTable {
public static final String TABLE_NAME = "city";
public static final String _ID = "_id";
public static final String NAME = "name";
public static final Uri CONTENT_URI =
Uri.parse("content://" + CityProvider.AUTHORITY + "/" + TABLE_NAME);
public static final Uri CONTENT_BASE_URI =
Uri.parse("content://" + CityProvider.AUTHORITY + "/" + TABLE_NAME + "/");
}
DataBaseHelper.java:
public class DataBaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "city.db";
private static final int DATABASE_VERSION = 1;
public DataBaseHelper(Context context, int version) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + CityTable.TABLE_NAME + " ("
+ CityTable._ID + " INTEGER PRIMARY KEY,"
+ CityTable.NAME + " TEXT"
+ ");");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS notes");
onCreate(db);
}
}
再实现提供CityTale数据的ContentProvider:
CityProvider.java:
public class CityProvider extends ContentProvider {
public static final String AUTHORITY = "com.example.cityloader.provider";
private static final int MAIN = 1;
private static final int MAIN_ID = 2;
private DataBaseHelper mDataBaseHelper;
private final UriMatcher mUriMatcher;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.com.example.cityloader.citytable";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.com.example.cityloader.citytable";
public CityProvider() {
mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mUriMatcher.addURI(AUTHORITY, CityTable.TABLE_NAME, MAIN);
mUriMatcher.addURI(AUTHORITY, CityTable.TABLE_NAME + "/#", MAIN_ID);
}
@Override
public boolean onCreate() {
mDataBaseHelper = new DataBaseHelper(getContext());
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
switch (mUriMatcher.match(uri)) {
case MAIN:
break;
case MAIN_ID:
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
new String[] { uri.getLastPathSegment() });
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
SQLiteDatabase db = mDataBaseHelper.getReadableDatabase();
Cursor c = db.query(CityTable.TABLE_NAME, null, selection,
selectionArgs, null, null, null);
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
}
@Override
public String getType(Uri uri) {
switch (mUriMatcher.match(uri)) {
case MAIN:
return CONTENT_TYPE;
case MAIN_ID:
return CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
if (mUriMatcher.match(uri) != MAIN) {
throw new IllegalArgumentException("Unknown URI " + uri);
}
SQLiteDatabase db = mDataBaseHelper.getWritableDatabase();
long rowId = db.insert(CityTable.TABLE_NAME, null, values);
if (rowId > 0) {
Uri noteUri = ContentUris.withAppendedId(
CityTable.CONTENT_BASE_URI, rowId);
getContext().getContentResolver().notifyChange(noteUri, null);
return noteUri;
}
throw new SQLException("Failed to insert row into " + uri);
}
@Override
public int delete(Uri uri, String whereClause, String[] whereArgs) {
SQLiteDatabase db = mDataBaseHelper.getWritableDatabase();
String finalWhere;
int count = 0;
switch (mUriMatcher.match(uri)) {
case MAIN:
count = db.delete(CityTable.TABLE_NAME, whereClause, whereArgs);
break;
case MAIN_ID:
finalWhere = DatabaseUtils.concatenateWhere(CityTable._ID + " = "
+ ContentUris.parseId(uri), whereClause);
count = db.delete(CityTable.TABLE_NAME, finalWhere, whereArgs);
break;
default:
throw new SQLException("Failed to insert row into " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
@Override
public int update(Uri uri, ContentValues values, String whereClause,
String[] whereArgs) {
SQLiteDatabase db = mDataBaseHelper.getWritableDatabase();
int count = 0;
String finalWhere;
switch (mUriMatcher.match(uri)) {
case MAIN:
count = db.update(CityTable.TABLE_NAME, values, whereClause,
whereArgs);
break;
case MAIN_ID:
finalWhere = DatabaseUtils.concatenateWhere(CityTable._ID + " = "
+ ContentUris.parseId(uri), whereClause);
count = db.update(CityTable.TABLE_NAME, values, finalWhere,
whereArgs);
break;
default:
throw new SQLException("Failed to insert row into " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
}
在insert,delete,update方法的最后,调用getContext().getContentResolver().notifyChange(uri, null);来通知内容的改变。
接着,fragment实现 LoaderManager.LoaderCallbacks
@Override
public Loader onCreateLoader(int id, Bundle args) {
CursorLoader cl = new CursorLoader(getActivity(),
CityTable.CONTENT_URI, null, null, null, null);
cl.setUpdateThrottle(1000); // update at most every 2 seconds.
return cl;
}
@Override
public void onLoadFinished(Loader loader, Cursor data) {
mAdapter.swapCursor(data);
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
@Override
public void onLoaderReset(Loader loader) {
mAdapter.swapCursor(null);
}
cl.setUpdateThrottle(1000);指定了,数据发生变化时,过1秒再刷新内容。
接下来,在菜单中添加插入/删除数据的操作:
@Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
menu.add(Menu.NONE, POPULATE_ID, 0, "Populate")
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
menu.add(Menu.NONE, CLEAR_ID, 0, "Clear")
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
}
@Override public boolean onOptionsItemSelected(MenuItem item) {
final ContentResolver cr = getActivity().getContentResolver();
switch (item.getItemId()) {
case POPULATE_ID:
if (mPopulatingTask != null) {
mPopulatingTask.cancel(false);
}
mPopulatingTask = new AsyncTask() {
@Override protected Void doInBackground(Void... params) {
for (int i = 0; i < cities.length; i++) {
if (isCancelled()) {
break;
}
ContentValues values = new ContentValues();
values.put(CityTable.NAME, cities[i]);
cr.insert(CityTable.CONTENT_URI, values);
try {
Thread.sleep(250);
} catch (InterruptedException e) {
}
}
return null;
}
};
mPopulatingTask.executeOnExecutor(
AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
return true;
case CLEAR_ID:
if (mPopulatingTask != null) {
mPopulatingTask.cancel(false);
mPopulatingTask = null;
}
AsyncTask task = new AsyncTask() {
@Override protected Void doInBackground(Void... params) {
cr.delete(CityTable.CONTENT_URI, null, null);
return null;
}
};
task.execute((Void[])null);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
之后,在manifest文件中增加ContentProvider的声明
最后,在Activity中显示Fragment
MainActivity.java:
public class MainActivity extends ActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FragmentManager fm = getFragmentManager();
if (fm.findFragmentById(android.R.id.content) == null) {
CityListFragment list = new CityListFragment();
fm.beginTransaction().add(android.R.id.content, list).commit();
}
}
}
CityListFragment.java完整代码:
public class CityListFragment extends ListFragment implements
LoaderManager.LoaderCallbacks {
static final int POPULATE_ID = Menu.FIRST;
static final int CLEAR_ID = Menu.FIRST + 1;
SimpleCursorAdapter mAdapter;
AsyncTask mPopulatingTask;
String[] cities = { "北京", "上海", "广州", "深圳" };
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
menu.add(Menu.NONE, POPULATE_ID, 0, "Populate").setShowAsAction(
MenuItem.SHOW_AS_ACTION_IF_ROOM);
menu.add(Menu.NONE, CLEAR_ID, 0, "Clear").setShowAsAction(
MenuItem.SHOW_AS_ACTION_IF_ROOM);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final ContentResolver cr = getActivity().getContentResolver();
switch (item.getItemId()) {
case POPULATE_ID:
if (mPopulatingTask != null) {
mPopulatingTask.cancel(false);
}
mPopulatingTask = new AsyncTask() {
@Override
protected Void doInBackground(Void... params) {
for (int i = 0; i < cities.length; i++) {
if (isCancelled()) {
break;
}
ContentValues values = new ContentValues();
values.put(CityTable.NAME, cities[i]);
cr.insert(CityTable.CONTENT_URI, values);
try {
Thread.sleep(250);
} catch (InterruptedException e) {
}
}
return null;
}
};
mPopulatingTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
(Void[]) null);
return true;
case CLEAR_ID:
if (mPopulatingTask != null) {
mPopulatingTask.cancel(false);
mPopulatingTask = null;
}
AsyncTask task = new AsyncTask() {
@Override
protected Void doInBackground(Void... params) {
cr.delete(CityTable.CONTENT_URI, null, null);
return null;
}
};
task.execute((Void[]) null);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setEmptyText("No data.");
setHasOptionsMenu(true);
mAdapter = new SimpleCursorAdapter(getActivity(),
android.R.layout.simple_list_item_1, null,
new String[] { CityTable.NAME },
new int[] { android.R.id.text1 }, 0);
setListAdapter(mAdapter);
setListShown(false);
getLoaderManager().initLoader(0, null, this);
}
@Override
public Loader onCreateLoader(int id, Bundle args) {
CursorLoader cl = new CursorLoader(getActivity(),
CityTable.CONTENT_URI, null, null, null, null);
cl.setUpdateThrottle(1000); // update at most every 2 seconds.
return cl;
}
@Override
public void onLoadFinished(Loader loader, Cursor data) {
mAdapter.swapCursor(data);
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
@Override
public void onLoaderReset(Loader loader) {
mAdapter.swapCursor(null);
}
}