1. 程式人生 > >Content Provider 之 最終彈 實戰體驗跨程式資料共享(結合SQLiteDemo)

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,

9125154-9122ceb510408fe9.png

會彈出如圖所示:

9125154-633d1994f088dbd0.png

這裡我們將內容提供器命名為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)的解釋參考:

9125154-a51816313b02add0.png
  • 然後是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檔案看一下:

9125154-912f53cceaf73cc9.png

可以看到<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);
            }
        });
    }
}

其中值得注意的是:

9125154-b804e3169fd8b3db.png

從以上程式碼中,我們可以看到DIR型別常量匹配的,selection, selectionArgs引數位是由呼叫本內容提供器的時候由呼叫方程式提供的,
ITEM則不同,uri中已經包含了ID,我們可以使用getPathSegments將之get出來再使用,即ITEM型別常量匹配的,呼叫方程式無需提供selection, selectionArgs引數位(如下方的程式碼截圖),uri中已經包含了資訊,處理方法也在內容提供器中寫好了。
(這一點在呼叫getContentResolver().update()以及getContentResolver().delete()的時候都一樣)

9125154-3598b1ed14d82ea9.png

下面進行程式碼的簡析:
分別在這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專案,效果圖如下:

9125154-809c95d655e7de4b.png
點選一下AddToBook按鈕,此時資料便已經新增到DatabaseTest程式的資料庫中了,
可以點選QueryFromBook按鈕來檢查一下,列印日誌如圖:
9125154-7bf5e4a4623ddda2.png

這裡可以看到DatabaseTest程式中只有我們剛剛新增的一條資料,
databaseTest的SQLite資料庫是我們在點選Add To Book的時候,試圖訪問DatabaseTest的內容提供器,由此DatabaseTest的內容提供器(DatabaseProvider)會觸發DatabaseProvider.java中的onCreate()方法,如下,

建立了資料庫之後,便添加了添加了一條資料,由此DatabaseTest程式中只有我們剛剛新增的那一條資料而已。

9125154-571755a8ca8736a6.png

點選一下Update Book按鈕來更新資料,再點選一下Query From Book按鈕進行檢查,結果如圖:

9125154-d8c3f2bb884e54c9.png

最後點選Delete From Book按鈕刪除資料,此時再點選Query From Book按鈕就查詢不到資料了。

至此跨程式共享資料功能便成功實現了。

現在不僅是ProviderTest程式,任何一個程式都可以輕鬆訪問DatabaseTest中的資料,同時絲毫不用擔心隱私資料洩漏的問題。