链接:https://developer.android.com/guide/components/loaders.html
android3.0引入,在activity和fragment里面使用loaders异步加载数据更简单。
Loader API Summary
略过
Using Loaders in an Application
在应用中使用loader,基本上都是下面的几部分:
- An
Activity
orFragment
. - An instance of the
LoaderManager
. - A
CursorLoader
to load data backed by aContentProvider
. Alternatively, you can implement your own subclass ofLoader
orAsyncTaskLoader
to load data from some other source. - An implementation for
LoaderManager.LoaderCallbacks
. This is where you create new loaders and manage your references to existing loaders. - A way of displaying the loader's data, such as a
SimpleCursorAdapter
. - A data source, such as a
ContentProvider
, when using aCursorLoader
.
Starting a Loader
在一个activity或fragment中,LoaderManager可以管理一个或多个Loader实例。但是LoaderManager只能有一个。
通常,Loader的初始化是在activity的onCreate()方法中,或者是fragment的onActivityCreated()方法中:
// Prepare the loader. Either re-connect with an existing one, // or start a new one. getLoaderManager().initLoader(0, null, this);
initLoader的三个参数介绍:
1.Loader的ID,唯一的。
2.可选的参数。Bundle类型
3.LoaderManager.LoaderCallbacks的实现
initLoader()方法可以确保loader被初始化和激活了,但是这可能会有两种结果:
1.Loader的id已经存在了,直接使用存在的那个Loader。
2.Loader不存在,initLoader()会触发LoaderManager.LoaderCallbacks接口的onCreateLoader()方法。在这个方法里,你应该实例化并返回一个新的Loader。
在调用initLoader的时候,如果Loader已经存在了,并且数据也加载完成了,这时会直接回调LoaderManager.LoaderCallbacks接口的onLoadFinished()方法。所以要注意这一点。
另外,我们不需要保存Loader的引用,因为LoaderManager会自动为我们管理Loader。LoaderManager会自动开始和停止数据的加载,并维护Loader的状态和其关联的数据。
Restarting a Loader
如果你想丢弃之前加载的旧数据,重新加载的话,使用restartLoader():
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.LoaderCallbacks包括下面3个方法:
onCreateLoader()
— Instantiate and return a newLoader
for the given ID.
onLoadFinished()
— Called when a previously created loader has finished its load.
onLoaderReset()
— Called when a previously created loader is being reset, thus making its data unavailable.
在onCreateLoader()
里面实例化一个Loader,代码:
// 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"); }
onLoadFinished
之前实例化的Loader加载完数据后,这个方法就会被调用。这个方法保证会在释放就数据之前调用,所以我们应该在这个时候释放掉旧数据,但是不用我们自己操作,Loader会自己处理好。
Loader会自动释放数据,一旦它知道应用不需要再使用这些数据了。举个栗子:如果你使用的是CursorLoader,那么你就不应该自己调用close()方法来释放数据。
如果Loader在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
之前实例化的Loader被重置时调用
This method is called when a previously created loader is being reset, thus making its data unavailable. This callback lets you find out when the data is about to be released so you can remove your reference to it.
This implementation calls swapCursor()
with a value of 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); }
Example
/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.apis.app; import android.app.Activity; import android.app.FragmentManager; import android.app.ListFragment; import android.app.LoaderManager; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.CursorLoader; import android.content.Loader; import android.content.UriMatcher; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.ListView; import android.widget.SimpleCursorAdapter; import java.util.HashMap; /** * Demonstration of bottom to top implementation of a content provider holding * structured data through displaying it in the UI, using throttling to reduce * the number of queries done when its data changes. */ public class LoaderThrottle extends Activity { // Debugging. static final String TAG = "LoaderThrottle"; /** * The authority we use to get to our sample provider. */ public static final String AUTHORITY = "com.example.android.apis.app.LoaderThrottle"; /** * Definition of the contract for the main table of our provider. */ public static final class MainTable implements BaseColumns { // This class cannot be instantiated private MainTable() {} /** * The table name offered by this provider */ public static final String TABLE_NAME = "main"; /** * The content:// style URL for this table */ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/main"); /** * The content URI base for a single row of data. Callers must * append a numeric row id to this Uri to retrieve a row */ public static final Uri CONTENT_ID_URI_BASE = Uri.parse("content://" + AUTHORITY + "/main/"); /** * The MIME type of {@link #CONTENT_URI}. */ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.example.api-demos-throttle"; /** * The MIME type of a {@link #CONTENT_URI} sub-directory of a single row. */ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.example.api-demos-throttle"; /** * The default sort order for this table */ public static final String DEFAULT_SORT_ORDER = "data COLLATE LOCALIZED ASC"; /** * Column name for the single column holding our data. * <P>Type: TEXT</P> */ public static final String COLUMN_NAME_DATA = "data"; } /** * This class helps open, create, and upgrade the database file. */ static class DatabaseHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "loader_throttle.db"; private static final int DATABASE_VERSION = 2; DatabaseHelper(Context context) { // calls the super constructor, requesting the default cursor factory. super(context, DATABASE_NAME, null, DATABASE_VERSION); } /** * * Creates the underlying database with table name and column names taken from the * NotePad class. */ @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + MainTable.TABLE_NAME + " (" + MainTable._ID + " INTEGER PRIMARY KEY," + MainTable.COLUMN_NAME_DATA + " TEXT" + ");"); } /** * * Demonstrates that the provider must consider what happens when the * underlying datastore is changed. In this sample, the database is upgraded the database * by destroying the existing data. * A real application should upgrade the database in place. */ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // Logs that the database is being upgraded Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data"); // Kills the table and existing data db.execSQL("DROP TABLE IF EXISTS notes"); // Recreates the database with a new version onCreate(db); } } /** * A very simple implementation of a content provider. */ public static class SimpleProvider extends ContentProvider { // A projection map used to select columns from the database private final HashMap<String, String> mNotesProjectionMap; // Uri matcher to decode incoming URIs. private final UriMatcher mUriMatcher; // The incoming URI matches the main table URI pattern private static final int MAIN = 1; // The incoming URI matches the main table row ID URI pattern private static final int MAIN_ID = 2; // Handle to a new DatabaseHelper. private DatabaseHelper mOpenHelper; /** * Global provider initialization. */ public SimpleProvider() { // Create and initialize URI matcher. mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); mUriMatcher.addURI(AUTHORITY, MainTable.TABLE_NAME, MAIN); mUriMatcher.addURI(AUTHORITY, MainTable.TABLE_NAME + "/#", MAIN_ID); // Create and initialize projection map for all columns. This is // simply an identity mapping. mNotesProjectionMap = new HashMap<String, String>(); mNotesProjectionMap.put(MainTable._ID, MainTable._ID); mNotesProjectionMap.put(MainTable.COLUMN_NAME_DATA, MainTable.COLUMN_NAME_DATA); } /** * Perform provider creation. */ @Override public boolean onCreate() { mOpenHelper = new DatabaseHelper(getContext()); // Assumes that any failures will be reported by a thrown exception. return true; } /** * Handle incoming queries. */ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // Constructs a new query builder and sets its table name SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables(MainTable.TABLE_NAME); switch (mUriMatcher.match(uri)) { case MAIN: // If the incoming URI is for main table. qb.setProjectionMap(mNotesProjectionMap); break; case MAIN_ID: // The incoming URI is for a single row. qb.setProjectionMap(mNotesProjectionMap); qb.appendWhere(MainTable._ID + "=?"); selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] { uri.getLastPathSegment() }); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } if (TextUtils.isEmpty(sortOrder)) { sortOrder = MainTable.DEFAULT_SORT_ORDER; } SQLiteDatabase db = mOpenHelper.getReadableDatabase(); Cursor c = qb.query(db, projection, selection, selectionArgs, null /* no group */, null /* no filter */, sortOrder); c.setNotificationUri(getContext().getContentResolver(), uri); return c; } /** * Return the MIME type for an known URI in the provider. */ @Override public String getType(Uri uri) { switch (mUriMatcher.match(uri)) { case MAIN: return MainTable.CONTENT_TYPE; case MAIN_ID: return MainTable.CONTENT_ITEM_TYPE; default: throw new IllegalArgumentException("Unknown URI " + uri); } } /** * Handler inserting new data. */ @Override public Uri insert(Uri uri, ContentValues initialValues) { if (mUriMatcher.match(uri) != MAIN) { // Can only insert into to main URI. throw new IllegalArgumentException("Unknown URI " + uri); } ContentValues values; if (initialValues != null) { values = new ContentValues(initialValues); } else { values = new ContentValues(); } if (values.containsKey(MainTable.COLUMN_NAME_DATA) == false) { values.put(MainTable.COLUMN_NAME_DATA, ""); } SQLiteDatabase db = mOpenHelper.getWritableDatabase(); long rowId = db.insert(MainTable.TABLE_NAME, null, values); // If the insert succeeded, the row ID exists. if (rowId > 0) { Uri noteUri = ContentUris.withAppendedId(MainTable.CONTENT_ID_URI_BASE, rowId); getContext().getContentResolver().notifyChange(noteUri, null); return noteUri; } throw new SQLException("Failed to insert row into " + uri); } /** * Handle deleting data. */ @Override public int delete(Uri uri, String where, String[] whereArgs) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); String finalWhere; int count; switch (mUriMatcher.match(uri)) { case MAIN: // If URI is main table, delete uses incoming where clause and args. count = db.delete(MainTable.TABLE_NAME, where, whereArgs); break; // If the incoming URI matches a single note ID, does the delete based on the // incoming data, but modifies the where clause to restrict it to the // particular note ID. case MAIN_ID: // If URI is for a particular row ID, delete is based on incoming // data but modified to restrict to the given ID. finalWhere = DatabaseUtils.concatenateWhere( MainTable._ID + " = " + ContentUris.parseId(uri), where); count = db.delete(MainTable.TABLE_NAME, finalWhere, whereArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } /** * Handle updating data. */ @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; String finalWhere; switch (mUriMatcher.match(uri)) { case MAIN: // If URI is main table, update uses incoming where clause and args. count = db.update(MainTable.TABLE_NAME, values, where, whereArgs); break; case MAIN_ID: // If URI is for a particular row ID, update is based on incoming // data but modified to restrict to the given ID. finalWhere = DatabaseUtils.concatenateWhere( MainTable._ID + " = " + ContentUris.parseId(uri), where); count = db.update(MainTable.TABLE_NAME, values, finalWhere, whereArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); FragmentManager fm = getFragmentManager(); // Create the list fragment and add it as our sole content. if (fm.findFragmentById(android.R.id.content) == null) { ThrottledLoaderListFragment list = new ThrottledLoaderListFragment(); fm.beginTransaction().add(android.R.id.content, list).commit(); } } public static class ThrottledLoaderListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Cursor> { // Menu identifiers static final int POPULATE_ID = Menu.FIRST; static final int CLEAR_ID = Menu.FIRST+1; // 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; // Task we have running to populate the database. AsyncTask<Void, Void, Void> mPopulatingTask; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setEmptyText("No data. Select 'Populate' to fill with data from Z to A at a rate of 4 per second."); setHasOptionsMenu(true); // Create an empty adapter we will use to display the loaded data. mAdapter = new SimpleCursorAdapter(getActivity(), android.R.layout.simple_list_item_1, null, new String[] { MainTable.COLUMN_NAME_DATA }, new int[] { android.R.id.text1 }, 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) { 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<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { for (char c='Z'; c>='A'; c--) { if (isCancelled()) { break; } StringBuilder builder = new StringBuilder("Data "); builder.append(c); ContentValues values = new ContentValues(); values.put(MainTable.COLUMN_NAME_DATA, builder.toString()); cr.insert(MainTable.CONTENT_URI, values); // Wait a bit between each insert. 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<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { cr.delete(MainTable.CONTENT_URI, null, null); return null; } }; task.execute((Void[])null); return true; default: return super.onOptionsItemSelected(item); } } @Override public void onListItemClick(ListView l, View v, int position, long id) { // Insert desired behavior here. Log.i(TAG, "Item clicked: " + id); } // These are the rows that we will retrieve. static final String[] PROJECTION = new String[] { MainTable._ID, MainTable.COLUMN_NAME_DATA, }; public Loader<Cursor> onCreateLoader(int id, Bundle args) { CursorLoader cl = new CursorLoader(getActivity(), MainTable.CONTENT_URI, PROJECTION, null, null, null); cl.setUpdateThrottle(2000); // update at most every 2 seconds. return cl; } public void onLoadFinished(Loader<Cursor> loader, Cursor data) { mAdapter.swapCursor(data); // The list should now be shown. if (isResumed()) { setListShown(true); } else { setListShownNoAnimation(true); } } public void onLoaderReset(Loader<Cursor> loader) { mAdapter.swapCursor(null); } } }
UriMatcher类使用介绍:
//常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码
UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//如果match()方法匹配content://cn.xxt.provider.personprovider/person路径,返回匹配码为1
sMatcher.addURI(“cn.xxt.provider.personprovider”, “person”, 1);//添加需要匹配uri,如果匹配就会返回匹配码
//如果match()方法匹配content://cn.xxt.provider.personprovider/person/230路径,返回匹配码为2
sMatcher.addURI(“cn.xxt.provider.personprovider”, “person/#”, 2);//#号为通配符
SQLiteQueryBuilder使用介绍:
SQLiteQueryBuilder 是一个构造SQL查询语句的辅助类。
sUriMatcher.match(uri),根据返回值可以判断这次查询请求时,它是请求全部数据还是某个id的数据。
如果返回值是MAIN,那么只需要执行qb.setTables(MainTable.TABLE_NAME)语句就可以了。
如果返回值是MAIN_ID,那么还需要将where部分的参数设置进去,代码为qb.appendWhere(MainTable._ID + "=?")。
SQLiteDatabase db = mOpenHelper.getReadableDatabase(),得到一个可读的SQLiteDatabase 实例。
Cursor c = qb.query(db, projection, selection, selectionArgs, null,null, orderBy)语句,这个查询类似于一个标准的SQL查询,
但是这个查询是SQLiteQueryBuilder 来发起的,而不是SQLiteDatabase 直接发起的,所以在参数方面略有不同。
这个函数为 query(SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit)下边将各个参数介绍一下:
第一个参数为要查询的数据库实例。
第二个参数是一个字符串数组,里边的每一项代表了需要返回的列名。
第三个参数相当于SQL语句中的where部分。
第四个参数是一个字符串数组,里边的每一项依次替代在第三个参数中出现的问号( )。
第五个参数相当于SQL语句当中的groupby部分。
第六个参数相当于SQL语句当中的having部分。
第七个参数描述是怎么进行排序。
第八个参数相当于SQL当中的limit部分,控制返回的数据的个数。