我們在這一篇裡, 想再說明另一種調用方式: SQLite + ContentProvider + Loader.
這種方式是透過 Loader 去調用 ContentProvider,
ContentProvider 再呼叫 SQLite 執行增刪改查的動作.
使用 Loader 有兩個好處,
一. 會異步更新資料 (Asynchronous Loading Data), 也就是不會佔到 Main Thread 的資源,
二. 可以監視 Data 的改變, 當 Data 改變時會通知 Main Thread.
在 Android Developer 的文件裡, 是建議我們使用 Loader 的.
SQLite + ContentProvider + Loader 的使用
使用上我們一樣分成幾個步驟:
1. 寫 ProductTable, 在這個 class 裡存放我們的資料檔名稱, 以及 provider 需要的路徑
2. 寫 ProductDBHelper ( 繼承自 SQLiteOpenHelper )
3. 寫 ProductProvider ( 繼承自 ContentProvider )
4. 在主程式裡調用
5. 在 AndroidManifest.xml 裡註冊 provider
我們的目標一樣是做上一篇的那張表, 並顯示在 ListView 上.
1. 寫 ProductTable
這個 ProductTable 繼承自 BaseColumns,
BaseColumns 包含了 _ID, _COUNT 兩個常數,
所以我們在設置常數的時候, 可以直接調用.
這裡我們要設置檔案的 Table 名稱, Table Column 的名稱,
以及 Provider 需要的 uri 路徑.
ProdcutTable.class
public final class ProductTable implements BaseColumns {
// This class cannot be instantiated
private ProductTable() {}
/**
* The table name offered by this provider
*/
public static final String TABLE_NAME = "product";
/**
* The content:// style URL for this table
*/
public static final Uri CONTENT_URI = Uri.parse("content://" + MainActivity.AUTHORITY + "/"+TABLE_NAME);
/**
* 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://" + MainActivity.AUTHORITY + "/"+TABLE_NAME+"/");
/**
* 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 = _ID+" COLLATE LOCALIZED ASC";
/**
* Column name for the single column holding our data.
* <P>Type: TEXT</P>
*/
public static final String COLUMN_NAME_DATA = "product_name";
}
2. 寫 ProductDBHelper
ProductDBHelper 繼承自 SQLiteOpenHelper, 用來執行 SQLite 的指令.
ProductDBHelper.class
public class ProductDBHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "productDB2.db";
private static final int DATABASE_VERSION = 2;
ProductDBHelper(Context context) {
// calls the super constructor, requesting the default cursor factory.
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + ProductTable.TABLE_NAME + " ("
+ ProductTable._ID + " INTEGER PRIMARY KEY,"
+ ProductTable.COLUMN_NAME_DATA + " TEXT"
+ ");");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Kills the table and existing data
db.execSQL("DROP TABLE IF EXISTS notes");
// Recreates the database with a new version
onCreate(db);
}
}
3. 寫 ProductProvider
我們透過 ProductProvider 來調用 ProductDBHelper,
這樣之後在主程式裡使用的時候, 就只需要管理 ProductProvider 這個接口就好.
ProductProvider.class
public class ProductProvider 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 ProductDBHelper mOpenHelper;
/**
* Global provider initialization.
*/
public ProductProvider() {
// Create and initialize URI matcher.
mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mUriMatcher.addURI(MainActivity.AUTHORITY, ProductTable.TABLE_NAME, MAIN);
mUriMatcher.addURI(MainActivity.AUTHORITY, ProductTable.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(ProductTable._ID, ProductTable._ID);
mNotesProjectionMap.put(ProductTable.COLUMN_NAME_DATA, ProductTable.COLUMN_NAME_DATA);
}
/**
* Perform provider creation.
*/
@Override
public boolean onCreate() {
mOpenHelper = new ProductDBHelper(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(ProductTable.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(ProductTable._ID + "=?");
selectionArgs = DatabaseUtilsCompat.appendSelectionArgs(selectionArgs,
new String[] { uri.getLastPathSegment() });
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = ProductTable.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 ProductTable.CONTENT_TYPE;
case MAIN_ID:
return ProductTable.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();
}
// 如果傳進來的 initialValues 是 null, 讓資料為 ""
if (values.containsKey(ProductTable.COLUMN_NAME_DATA) == false) {
values.put(ProductTable.COLUMN_NAME_DATA, "");
}
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
long rowId = db.insert(ProductTable.TABLE_NAME, null, values);
// If the insert succeeded, the row ID exists.
if (rowId > 0) {
Uri noteUri = ContentUris.withAppendedId(ProductTable.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(ProductTable.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 = DatabaseUtilsCompat.concatenateWhere(
ProductTable._ID + " = " + ContentUris.parseId(uri), where);
count = db.delete(ProductTable.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(ProductTable.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 = DatabaseUtilsCompat.concatenateWhere(
ProductTable._ID + " = " + ContentUris.parseId(uri), where);
count = db.update(ProductTable.TABLE_NAME, values, finalWhere, whereArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
}
這裡用了一個技巧是使用 UriMatcher 透過路徑來分辨是要對 table 或者是 row 進行刪,查的動作, 也是挺實用的.
4. 在主程式裡調用
在 MainActivity 要使用 LoaderManager 必須繼承自 FragmentActivity,
並且 implements LoaderManager.LoaderCallbacks<Cursor>,
這樣就能有 Loader 的回呼函數: onCreateLoader(), onLoadFinished(), onLoaderReset().
實作上, 我們取得 ContentResolver (可以用來執行 ContentProvider 裡的方法),
接著存入幾筆資料, 再利用 Loader 讀回來.
MainActivity.class
public class MainActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks<Cursor>{
/**
* The authority we use to get to our sample provider.
*/
public static final String AUTHORITY = "com.example.learnsqliteusingloader";
private ListView mainList;
// This is the Adapter being used to display the list's data.
private SimpleCursorAdapter mAdapter;
@SuppressLint("NewApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mainList = (ListView) findViewById (R.id.main_list);
// Create an empty adapter we will use to display the loaded data.
mAdapter = new SimpleCursorAdapter(this,
R.layout.item_list, null,
new String[] { ProductTable.COLUMN_NAME_DATA },
new int[] { R.id.text_list }, 0);
mainList.setAdapter(mAdapter);
// 寫入資料
ContentResolver cr = getContentResolver();
for (int i=0;i<11;i++){
ContentValues values = new ContentValues();
values.put(ProductTable.COLUMN_NAME_DATA, "cup_"+ Integer.toString(i));
cr.insert(ProductTable.CONTENT_URI, values);
}
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
static final String[] PROJECTION = new String[] {
ProductTable._ID,
ProductTable.COLUMN_NAME_DATA,
};
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
CursorLoader cl = new CursorLoader(this, ProductTable.CONTENT_URI,
PROJECTION, null, null, null);
return cl;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mAdapter.swapCursor(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
}
5. 在 AndroidManifest.xml 裡註冊 provider
最後要在 AndroidManifest.xml 裡註冊 provider,
特別要注意 authority 的路徑要跟在 MainActivity.class 設的 AUTHORITY 一樣.
因為我們在 ProductTable.class 裡設的許多路徑, 也是跟著這個 AUTHORITY 設的.
必須要是這個路徑才能調用我們的 ProductProvider
AndroidManifest.xml
<provider android:name=".ProductProvider"
android:authorities="com.example.learnsqliteusingloader" />
範例圖: (原始碼連結)
沒有留言:
張貼留言