Android Loader(二) CursorLoader

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.LoaderCallbacks接口, 

onCreateLoader以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上。

工程目录:

Android Loader(二) CursorLoader_第1张图片

先定义表结构:

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

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;
	}
}

query方法中的 c.setNotificationUri(getContext().getContentResolver(), uri);注册了URi内容变化通知,Cursor会监听Uri内容的变化,当内容变化时,Cursor会自动更新数据。

在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);
	}
}

效果:

Android Loader(二) CursorLoader_第2张图片

你可能感兴趣的:(Android)