Content Provider 之 最終彈 實戰體驗跨程式資料共享(結合SQLiteDemo)
本模組共有四篇文章,參考郭神的《第一行程式碼》,對Content Provider的學習做一個詳細的筆記,大家可以一起交流一下:
簡單起見,我們還是在之前的DatabaseTest專案(點選前往碼雲地址)的基礎上繼續開發。
需求是:通過內容提供器來給它加人外部訪問介面。
程式設計的步驟:
1.在A程式中註冊內容提供器,寫好介面處理方法; 具體的,全域性變數:定義自定義程式碼常量,定義authority常量,宣告uriMatcher和DatabaseHelper物件; 1.1 內容提供器中增刪改查的程式設計步驟為: 1.1.1 呼叫例項化DatabaseHelper的get方法獲得SQLiteDatabase例項化物件; (get方法即getWritableDatabase或者getReadableDatabase) 1.1.2 接著,query需定義一個Cursor物件(cursor)用於接收返回結果; insert需定義一個Uri物件(urireturn)用於接收insert新增的資料行的uri update需定義一個int物件(updatedRows)用於接收受影響的行數; delete需定義一個int物件(deletedRows)用於接收受影響的行數(被刪除的行數); 1.1.3 使用switch語句進行對uri的判斷及判斷結果的處理; 2.在需要訪問A程式的內容提供器的程式中,構建對應的Uri,通過getContentResolver呼叫增刪改查即可;
下面開始詳細解析:
開啟DatabaseTest專案(點選前往碼雲地址),首先將MyDatabaseHelper中使用Toast彈出建立資料庫成功的提示去除掉,因為跨程式訪問時我們不能直接使用Toast(!!!!!)。然後建立一個內容提供器,右擊com.example.databasetest包—New—Other—ContentProvider,
會彈出如圖所示:
這裡我們將內容提供器命名為DatabaseProvider,
authority指定為com.example.databasetest.provider,
Exported屬性表示是否允許外部程式訪問我們的內容提供器,
Enabled屬性表示是否啟用這個內容提供器。
將兩個屬性都勾中,點選Finish完成建立。
接著我們修改DatabaseProvider:
package com.example.databasetest; import android.content.ContentProvider; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; public class DatabaseProvider extends ContentProvider { public static final int BOOK_DIR = 0; public static final int BOOK_ITEM = 1; public static final int CATEGORY_DIR = 2; public static final int CATEGORY_ITEM = 3; public static final String AUTHORITY = "com.example.databasetest.provider"; private static UriMatcher uriMatcher; private MyDatabaseHelper dbHelper; static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR); uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM); uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR); uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM); } @Override public boolean onCreate() { dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // 查詢資料 SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = null; switch (uriMatcher.match(uri)) { case BOOK_DIR: cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder); break; case BOOK_ITEM: String bookId = uri.getPathSegments().get(1); cursor = db.query("Book", projection, "id = ?", new String[] { bookId }, null, null, sortOrder); break; case CATEGORY_DIR: cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder); break; case CATEGORY_ITEM: String categoryId = uri.getPathSegments().get(1); cursor = db.query("Category", projection, "id = ?", new String[] { categoryId }, null, null, sortOrder); break; default: break; } return cursor; } @Override public Uri insert(Uri uri, ContentValues values) { // 新增資料 SQLiteDatabase db = dbHelper.getWritableDatabase(); Uri uriReturn = null; switch (uriMatcher.match(uri)) { case BOOK_DIR: case BOOK_ITEM: long newBookId = db.insert("Book", null, values); //插入後返回一個id!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);//id構造用於返回的URI!!!!!!!!!!!!!!!!!!!!!! break; case CATEGORY_DIR: case CATEGORY_ITEM: long newCategoryId = db.insert("Category", null, values); uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId); break; default: break; } return uriReturn; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // 更新資料 SQLiteDatabase db = dbHelper.getWritableDatabase(); int updatedRows = 0; switch (uriMatcher.match(uri)) { case BOOK_DIR: updatedRows = db.update("Book", values, selection, selectionArgs); break; case BOOK_ITEM: String bookId = uri.getPathSegments().get(1); updatedRows = db.update("Book", values, "id = ?", new String[] { bookId }); break; case CATEGORY_DIR: updatedRows = db.update("Category", values, selection, selectionArgs); break; case CATEGORY_ITEM: String categoryId = uri.getPathSegments().get(1); updatedRows = db.update("Category", values, "id = ?", new String[] { categoryId }); break; default: break; } return updatedRows; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { // 刪除資料 SQLiteDatabase db = dbHelper.getWritableDatabase(); int deletedRows = 0; switch (uriMatcher.match(uri)) { case BOOK_DIR: deletedRows = db.delete("Book", selection, selectionArgs); break; case BOOK_ITEM: String bookId = uri.getPathSegments().get(1); deletedRows = db.delete("Book", "id = ?", new String[] { bookId }); break; case CATEGORY_DIR: deletedRows = db.delete("Category", selection, selectionArgs); break; case CATEGORY_ITEM: String categoryId = uri.getPathSegments().get(1); deletedRows = db.delete("Category", "id = ?", new String[] { categoryId }); break; default: break; } return deletedRows; } @Override public String getType(Uri uri) { switch (uriMatcher.match(uri)) { case BOOK_DIR: return "vnd.android.cursor.dir/vnd.com.example.databasetest. provider.book"; case BOOK_ITEM: return "vnd.android.cursor.item/vnd.com.example.databasetest. provider.book"; case CATEGORY_DIR: return "vnd.android.cursor.dir/vnd.com.example.databasetest. provider.category"; case CATEGORY_ITEM: return "vnd.android.cursor.item/vnd.com.example.databasetest. provider.category"; } return null; } }
程式碼雖然很長,不過都不難理解,都是上一節學習過的內容。
首先在類的一開始,同樣是定義了4個常量,分別用於表示
訪問Book表中的所有資料;
訪問Book表中的單條資料;
訪問Category表中的所有資料;
訪問Category表中的單條資料。
然後在靜態程式碼塊裡對UriMatcher進行了初始化操作,將期望匹配的幾種URI格式添加了進去。接下來是onCreate()方法:
建立了一個MyDatabaseHelper的例項,然後返回true表示內容提供器初始化成功,這時資料庫就已經完成了建立或升級操作。接著是query()方法:
首先獲取到SQLiteDatabase例項,然後根據傳入的Uri引數判斷出使用者想要訪問哪張表,
再呼叫SQLiteDatabase的query()進行查詢,並將Cursor物件返回即可。
注意當訪問單條資料的時候有一個細節:
這裡呼叫了Uri物件的getpathSegments()
方法,它會將內容URI許可權之後的部分以“/“符號進行分割,並把分割後
的結果放入到一個字串列表中,那這個列表的第0個位置存放的就是路徑,第1個位置存放的就是id了。
得到了id之後,再通過selection和selectionArgs引數進行約束,就實現了查詢單條資料的功能。
getPathSegments().get(1)的解釋參考:
然後是insert()方法:
同樣先獲取到SQLiteDatabase例項,
然後根據傳入的Uri引數判斷出使用者想要往哪張表裡新增資料,
再呼叫SQLiteDatabase的insert()方法進行新增即可。
注意insert()方法要求返回一個能夠表示這條新增資料的URI,則這裡還需呼叫Uri.parse()方法來將一個內容URI解析成Uri物件,當然這個內容URI是以新增資料的id結尾的。接著是update()方法:
先獲取SQLiteDatabase例項,
然後根據傳入的Uri引數判斷出使用者想要更新哪張表裡的資料,
再呼叫SQLiteDatabase的update()方法進行更新即可,受影響的行數作為返回值返回。然後是delete()方法:
先獲取到SQLiteDatabase的例項,
然後根據傳入的Uri引數判斷出使用者想要刪除哪張表裡的資料,
再呼叫SQLiteDatabase的delete()方法進行刪除即可,被刪除的行數作為返回值返回。最後是getType()方法,這裡按照上一節中介紹的格式規則編寫即可。
至此內容提供器中的程式碼便全部編寫完了。
另外!!內容提供器一定要在AndroidMamfest.xml檔案中註冊才可以使用,
不過使用Androidstudio的快捷方式建立內容提供器的話,註冊會被自動完成。
開啟AndroidManifest.xmI檔案看一下:
可以看到<application>標籤內出現了一個新的標籤<provider>,我們使用它來註冊內容提供器DatabaseProvider。
其中android:name指定DatabaseProvider的類名,
android:authorities指定了DatabaseProvider的authority,
enabled和exported屬性則是根據我們剛才勾選的狀態自動生成的。
現在DatabaseTest這個專案便具備跨程式共享資料的功能了。
可以除錯一下:
首先將DatabaseTest程式從模擬器中刪除掉,防止遺留資料的造成干擾。
然後執行一下專案,將DatabaseTest程式重新安裝在模擬器上。
接著關閉掉DatabaseTest這個專案,並建立一個新專案ProviderTest,
接著通過這個程式去訪問DatabaseTest中的資料,
先編寫佈局檔案:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/add_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add To Book" />
<Button
android:id="@+id/query_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Query From Book" />
<Button
android:id="@+id/update_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Update Book" />
<Button
android:id="@+id/delete_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Delete From Book" />
</LinearLayout>
MainActivity.java:
package com.example.providertest;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private String newId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 新增資料
Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
ContentValues values = new ContentValues();
values.put("name", "A Clash of Kings");
values.put("author", "George Martin");
values.put("pages", 1040);
values.put("price", 55.55);
Uri newUri = getContentResolver().insert(uri, values);
newId = newUri.getPathSegments().get(1);
}
});
Button queryData = (Button) findViewById(R.id.query_data);
queryData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 查詢資料
Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(cursor. getColumnIndex("name"));
String author = cursor.getString(cursor. getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex ("pages"));
double price = cursor.getDouble(cursor. getColumnIndex("price"));
Log.d("MainActivity", "book name is " + name);
Log.d("MainActivity", "book author is " + author);
Log.d("MainActivity", "book pages is " + pages);
Log.d("MainActivity", "book price is " + price);
}
cursor.close();
}
}
});
Button updateData = (Button) findViewById(R.id.update_data);
updateData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 更新資料
Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + newId);
ContentValues values = new ContentValues();
values.put("name", "A Storm of Swords");
values.put("pages", 1216);
values.put("price", 24.05);
getContentResolver().update(uri, values, null, null);
}
});
Button deleteData = (Button) findViewById(R.id.delete_data);
deleteData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 刪除資料
Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + newId);
getContentResolver().delete(uri, null, null);
}
});
}
}
其中值得注意的是:
從以上程式碼中,我們可以看到
DIR
型別常量匹配的,selection, selectionArgs引數位是由呼叫本內容提供器的時候由呼叫方程式提供的,
而ITEM
則不同,uri中已經包含了ID,我們可以使用getPathSegments將之get出來再使用,即ITEM
型別常量匹配的,呼叫方程式無需提供selection, selectionArgs引數位(如下方的程式碼截圖),uri中已經包含了資訊,處理方法也在內容提供器中寫好了。
(這一點在呼叫getContentResolver().update()以及getContentResolver().delete()的時候都一樣)
下面進行程式碼的簡析:
分別在這4個按鈕的點選事件裡面處理了增刪改查的邏輯:
新增資料的時候:
首先呼叫Uri.parse()將內容URI解析成Uri物件,
把要新增的資料存放到ContentValues物件中,
呼叫ContentResolver的insert()方法執行新增操作即可。
注意insert()會返回一個Uri物件,這個物件中包含了新增資料的id,這裡用getPathSegments()將這個id取出,稍後會用到它;查詢資料的時候:
呼叫Uri.parse()將內容URI解析成Uri物件,
呼叫ContentResolver的query()方法去查詢資料,
查詢的結果存放在Cursor物件中,
對Cursor進行遍歷,從中取出查詢結果,並一一打印出來;更新資料的時候:
呼叫Uri.parse()將內容URI解析成Uri物件,
把想要更新的資料存放到ContentValues物件中,
呼叫ContentResolver的update()方法執行更新操作即可;
注意這裡為了不讓Book表中的其他行受到影響,
在呼叫Uri.parse()方法時,給內容URI的尾部增加了一個id,而這個id正是新增資料時所返回的。
也就是說這裡只更新剛剛新增的那條資料,不受影響Book表中的其他行。刪除資料的時候,
解析一個以id結尾的內容URI,
呼叫ContentResolver的delete()方法執行刪除操作就可以了,
由於我們在內容URI裡指定了一個id,因此只會刪掉擁有相應id的那行資料,不會影響Book表中的其他資料。
現在執行一下ProviderTest專案,效果圖如下:
點選一下AddToBook按鈕,此時資料便已經新增到DatabaseTest程式的資料庫中了,
可以點選QueryFromBook按鈕來檢查一下,列印日誌如圖:
這裡可以看到DatabaseTest程式中只有我們剛剛新增的一條資料,
databaseTest的SQLite資料庫是我們在點選Add To Book的時候,試圖訪問DatabaseTest的內容提供器,由此DatabaseTest的內容提供器(DatabaseProvider)會觸發DatabaseProvider.java中的onCreate()方法,如下,
建立了資料庫之後,便添加了添加了一條資料,由此DatabaseTest程式中只有我們剛剛新增的那一條資料而已。
點選一下Update Book按鈕來更新資料,再點選一下Query From Book按鈕進行檢查,結果如圖:
最後點選Delete From Book按鈕刪除資料,此時再點選Query From Book按鈕就查詢不到資料了。
至此跨程式共享資料功能便成功實現了。
現在不僅是ProviderTest程式,任何一個程式都可以輕鬆訪問DatabaseTest中的資料,同時絲毫不用擔心隱私資料洩漏的問題。