Android 四大組件(四) —— ContentProvider 知識體系

簡介

ContentProvider 用於應用程序間數據共享。比如系統的通訊錄,短信、媒體庫中的數據,都對外提供了 ContentProvider,使得我們可以很方便的訪問其中的數據。當然,我們也可以自定義 ContentProvider 爲其他程序提供數據,實現程序間的數據共享。ContentProvider 使用起來和數據庫非常類似,常用的方法就是增刪改查。

接下來我們先創建一個數據庫,再使用 ContentProvider 將其共享出去。

一、準備數據:創建 SQLite 數據庫

由於操作 SQLite 數據庫不是本文的重點,所以我們快速的過一遍。新建一個應用程序,包名是 com.example.contentproviderdemo

新建 MyDbHelper 類:

class MyDbHelper(context: Context, name: String, version: Int) : SQLiteOpenHelper(context, name, null, version) {
    private val createBook = """create table Book (
        id integer primary key autoincrement,
        name text,
        price real)
    """.trimMargin()
    private val createCategory = """create table Category (
        id integer primary key autoincrement,
        name text,
        code integer)
    """.trimMargin()

    override fun onCreate(db: SQLiteDatabase?) {
        db?.execSQL(createBook)
        db?.execSQL(createCategory)
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
    }
}

在這個類中,我們創建了兩個表,Book 和 Category,每個表都有一個 id 和兩個字段。

接下來在 MainActivity 中,創建這兩個表並插入幾行數據:

class MainActivity : AppCompatActivity() {

    private val db by lazy { MyDbHelper(this, "BookStore.db", 1).writableDatabase }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        btnCreate.setOnClickListener {
            db.apply {
                insert("Book", null, contentValuesOf("name" to "第一行代碼", "price" to 99.00))
                insert("Book", null, contentValuesOf("name" to "Android 源碼設計模式解析與實戰", "price" to 99.00))
                insert("Category", null, contentValuesOf("name" to "Android", "code" to 1))
                insert("Category", null, contentValuesOf("name" to "Android", "code" to 2))
            }
        }
    }
}

佈局文件中只有一個 id 爲 btnCreate 的按鈕,故不再給出佈局代碼。運行程序,點擊一次按鈕,兩個表就會被創建,並分別插入兩條數據,這樣我們數據的準備工作就完成了。

可以用 DataBase Navigator 插件查看這個數據庫文件,文件所在的路徑是 /data/data/包名/databases,文件名稱是 BookStore.db

Book 表:

id name price
1 第一行代碼 99
2 Android 源碼設計模式解析與實戰 99

Category 表:

id name price
1 Android 1
2 Android 2

Ok,數據準備好之後,我們就可以開始編寫 ContentProvider 了。

二、創建 ContentProvider

新建 MyContentProvider 類,繼承自 ContentProvider,並實現其中的抽象方法:

class MyContentProvider : ContentProvider() {

    override fun onCreate(): Boolean {
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
    }

    override fun update(
        uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ): Int {
    }

    override fun query(
        uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ): Cursor? {
    }

    override fun getType(uri: Uri): String? {
    }
}

一共有六個抽象方法需要實現。

  • onCreate 會在 ContentProvider 初始化時調用
  • insert、delete、update、query 分別對應共享數據的增刪改查
  • getType 用於獲取 Uri 對象所對應的 MIME 類型,暫時不理解 MIME 類型也沒關係,不妨把它記做固定寫法,它由三部分構成
    • 必須以 vnd 開頭
    • 如果內容 URI 以路徑結尾,則後接 android.cursor.dir/;如果以 id 結尾,則後接 android.cursor.item/
    • 最後接上 vnd.<authority>.<path>

看一下 MyContentProvider 的最終實現:

const val BookStore = "BookStore.db"
const val Book = "Book"
const val Category = "Category"
const val bookDir = 0
const val bookItem = 1
const val categoryDir = 2
const val categoryItem = 3
const val authority = "com.example.contentproviderdemo.provider"

class MyContentProvider : ContentProvider() {

    private lateinit var db: SQLiteDatabase
    private val uriMatcher by lazy {
        UriMatcher(UriMatcher.NO_MATCH).apply {
            addURI(authority, "book", bookDir)
            addURI(authority, "book/#", bookItem)
            addURI(authority, "category", categoryDir)
            addURI(authority, "category/#", categoryItem)
        }
    }

    override fun onCreate() = context?.let {
        db = MyDbHelper(it, BookStore, 1).writableDatabase
        true
    } ?: throw NullPointerException()

    override fun insert(uri: Uri, values: ContentValues?) = when (uriMatcher.match(uri)) {
        bookDir, bookItem -> {
            val newBookId = db.insert(Book, null, values)
            Uri.parse("content://$authority/book/$newBookId")
        }
        categoryDir, categoryItem -> {
            val newCategoryId = db.insert(Category, null, values)
            Uri.parse("content://$authority/category/$newCategoryId")
        }
        else -> null
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = when (uriMatcher.match(uri)) {
        bookDir -> db.delete(Book, selection, selectionArgs)
        bookItem -> db.delete(Book, "id = ?", arrayOf(uri.pathSegments[1]))
        categoryDir -> db.delete(Category, selection, selectionArgs)
        categoryItem -> db.delete(Category, "id = ?", arrayOf(uri.pathSegments[1]))
        else -> 0
    }

    override fun update(
        uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ) = when (uriMatcher.match(uri)) {
        bookDir -> db.update(Book, values, selection, selectionArgs)
        bookItem -> db.update(Book, values, "id = ?", arrayOf(uri.pathSegments[1]))
        categoryDir -> db.update(Category, values, selection, selectionArgs)
        categoryItem -> db.update(Category, values, "id = ?", arrayOf(uri.pathSegments[1]))
        else -> 0
    }

    @SuppressLint("Recycle")
    override fun query(
        uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ) = when (uriMatcher.match(uri)) {
        bookDir -> db.query(Book, projection, selection, selectionArgs, null, null, sortOrder)
        bookItem -> db.query(Book, projection, "id = ?", arrayOf(uri.pathSegments[1]), null, null, sortOrder)
        categoryDir -> db.query(Category, projection, selection, selectionArgs, null, null, sortOrder)
        categoryItem -> db.query(Category, projection, "id = ?", arrayOf(uri.pathSegments[1]), null, null, sortOrder)
        else -> null
    }

    override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
        bookDir -> "vnd.android.cursor.dir/vnd.$authority.book"
        bookItem -> "vnd.android.cursor.item/vnd.$authority.book"
        categoryDir -> "vnd.android.cursor.dir/vnd.$authority.category"
        categoryItem -> "vnd.android.cursor.item/vnd.$authority.category"
        else -> null
    }
}

代碼較長,但並不複雜。

  • 在 onCreate 方法中,初始化 SQLiteDatabase 變量 db,用於待會的數據庫操作
  • insert 方法中,根據 Uri 插入不同的表格,然後將插入數據的內容 Uri 返回
  • delete 方法中,根據 Uri 刪除不同的表格中的數據,然後將刪除的數據條數返回
  • update 方法中,根據 Uri 更新不同的表格中的數據,然後將更新的數據條數返回
  • query 方法中,根據 Uri 查詢不同的表格中的數據,將查詢出的 Cursor 對象返回
  • getType 方法中,根據上文所說的規則拼接出字符串返回即可

這幾個方法都用到了 Uri,那麼 Uri 是什麼呢?

其實 Uri 就相當於一個地址,它主要由三部分組成:前綴、authority 和 path。

  • 前綴 content:// 是固定格式,用來表示這是一個內容 Uri。
  • authority 一般是程序的包名加上 .provider,用於指定是哪個應用程序中的 ContentProvider,對應本例中的 com.example.contentproviderdemo.provider
  • path 指路徑,用於區分同一個程序中不同的數據表,對應本例中的 /book/category,path 後面還可以後綴一個 id,比如 /book/1 表示 Book 表中 id 爲 1 的元素

由此可知,從 Uri 中我們就可以知道需要操作的是哪個應用程序中的哪個表格,甚至精確到哪條數據。這是一個將地址封裝起來的思想,只不過它沒有用單獨的類封裝,而是封裝在一個字符串中,方便我們使用。

Uri 可以很方便的取出 path 中的每一個元素,uri.pathSegments 就是用來做這個的,它會將 path 中的每一部分分割出來,保存到列表中。如:

  • content://com.example.contentproviderdemo.provider/book 分割後, uri.pathSegments 中存儲的就是 [book]
  • content://com.example.contentproviderdemo.provider/book/1 分割後, uri.pathSegments 中存儲的就是 [book, 1]

所以當需要操作的是 item 時,我們使用了 uri.pathSegments[1] 表示 id。

這裏還用到了一個 UriMatcher 類,它是用來輔助我們匹配 URI 的,UriMatcher 類似於一個 HashMap<Uri, code>

  • 先通過 addURI(authority: String?, path: String?, code: Int) 方法往 UriMatcher 中添加了許多 URI
  • 然後再用 uriMatcher.match(uri) 方法來匹配傳入的 Uri,如果 addURI 時傳入的前兩個參數這樣拼接出來的 URI content://$authority/$path 和 match 方法傳入的 uri 一致,就會返回 addURI 方法中傳入的第三個參數 code
  • 如果 UriMatcher 中沒有任何一個 URI 能和傳入的 Uri 匹配上,則返回構造方法中傳入的默認參數 UriMatcher.NO_MATCH

ContentProvider 寫好後,需要在 AndroidManifest 中註冊:

<application
    ...>
    ...
    <provider
        android:name=".MyContentProvider"
        android:authorities="com.example.contentproviderdemo.provider"
        android:enabled="true"
        android:exported="true" />
</application>
  • exported 表示是否對外分享此 ContentProvider,默認是 false,我們需要對外分享,所以將其設置成 true
  • enabled 表示是否啓用此 ContentProvider,默認就是 true,不過爲了防止 Android 在以後的版本更新中修改默認值,我們最好把兩個屬性都設置好。

三、在其他應用程序中讀取此 ContentProvider

另外新建一個應用程序,編輯 MainActivity:

class MainActivity : AppCompatActivity() {
    private val bookUri by lazy { Uri.parse("content://com.example.contentproviderdemo.provider/book") }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        btnAdd.setOnClickListener {
            contentResolver.insert(bookUri, contentValuesOf("name" to "大話設計模式", "price" to 45.00))
        }
        btnDelete.setOnClickListener {
            contentResolver.delete(bookUri, "name = ?", arrayOf("大話設計模式"))
        }
        btnUpdate.setOnClickListener {
            contentResolver.update(bookUri, contentValuesOf("price" to 99.00), "name = ?", arrayOf("大話設計模式"))
        }
        btnQuery.setOnClickListener {
            val cursor = contentResolver.query(bookUri, null, null, null, null)
            cursor?.apply {
                while (moveToNext()) {
                    val name = getString(getColumnIndex("name"))
                    val price = getDouble(getColumnIndex("price"))
                    Log.d("~~~", "$name: $price")
                }
                close()
            }
        }
    }
}

佈局文件中只有四個 id 是 btnAdd、btnDelete、btnUpdate、btnQuery 的按鈕,故不再給出佈局代碼。

訪問 ContentProvider 中的數據需要藉助 ContentResolver 類,調用這個類的 insert、delete、update、query 方法時,就會回調我們剛纔寫的 ContentProvider 中的對應方法。

點擊 btnQuery,Log 如下:

~~~: 第一行代碼: 99.0
~~~: Android 源碼設計模式解析與實戰: 99.0

這就是我們剛纔的應用程序中創建的 Book 表中的數據。

點擊 btnAdd 後,再點擊 btnQuery,Log 如下:

~~~: 第一行代碼: 99.0
~~~: Android 源碼設計模式解析與實戰: 99.0
~~~: 大話設計模式: 45.0

說明我們添加數據成功了,再測試一下更新數據和刪除數據。

點擊 btnUpdate 後,再點擊 btnQuery,Log 如下:

~~~: 第一行代碼: 99.0
~~~: Android 源碼設計模式解析與實戰: 99.0
~~~: 大話設計模式: 99.0

點擊 btnDelete 後,再點擊 btnQuery,Log 如下:

~~~: 第一行代碼: 99.0
~~~: Android 源碼設計模式解析與實戰: 99.0

這就說明我們對上一個程序共享的 ContentProvider 數據的增刪改查操作都成功了。

四、藉助 ContentProvider 訪問系統通訊錄

前文說到,系統的通訊錄也爲我們提供了 ContentProvider,那麼我們就來嘗試查詢一下系統通訊錄的數據吧。

先打開通訊錄,添加一條數據:

在 AndroidManifest 中申請讀取通訊錄權限:

<uses-permission android:name="android.permission.READ_CONTACTS" />

MainActivity 中添加如下代碼:

import android.provider.ContactsContract.CommonDataKinds.Phone

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 1)
        val cursor = contentResolver.query(Phone.CONTENT_URI, null, null, null, null)
        cursor?.apply {
            while (moveToNext()) {
                val displayName = getString(getColumnIndex(Phone.DISPLAY_NAME))
                val number = getString(getColumnIndex(Phone.NUMBER))
                Log.d("~~~", "$displayName: $number")
            }
            close()
        }
    }
}

由於 Android 6.0 以後,READ_CONTACTS 被劃分爲危險權限,所以我們需要在程序運行時調用 requestPermissions 方法動態申請這個權限。由於動態申請權限不是本文的重點,所以筆者只是簡單的申請了一下,沒有處理用戶拒絕權限後的操作。

運行程序,同意權限後,輸出如下:

~~~: Alpinist Wang: (666) 666-666

說明我們訪問通訊錄數據成功了!

順便說一下查詢時不傳入 null 值的寫法,一個攜帶所有參數的 query 語句如下,和 SQLite 查詢一模一樣。事實上,這裏的參數傳到 ContentProvider 後,就是調用的 SQLite 的查詢:

val cursor = contentResolver.query(Phone.CONTENT_URI, arrayOf(Phone.DISPLAY_NAME, Phone.NUMBER), "${Phone.DISPLAY_NAME} = ?", arrayOf("Alpinist Wang"), Phone.DISPLAY_NAME)
cursor?.apply {
    while (moveToNext()) {
        val displayName = getString(getColumnIndex(Phone.DISPLAY_NAME))
        val number = getString(getColumnIndex(Phone.NUMBER))
        Log.d("~~~", "$displayName: $number")
    }
    close()
}

意思是篩選出 Phone.DISPLAY_NAME 的值爲 “Alpinist Wang” 的所有行,取出這些數據中的 Phone.DISPLAY_NAMEPhone.NUMBER 這兩列,最後按照 Phone.DISPLAY_NAME 排序。運行程序,輸出和剛纔一樣。

這就是通過 ContentProvider 讀取系統通訊錄的方法,不過要想對系統通訊錄進行增刪改,和我們自定義的 ContentProvider 有點出入的,因爲通訊錄涉及多個表,所以必須同時修改多個表才行,感興趣的讀者可以自行查閱文檔瞭解。

以上就是 ContentProvider 的使用方式,至此,我們已將 Android 四大組件都梳理了一遍,對其他組件感興趣的讀者可以訪問本專欄的其他文章查看。

參考文章

《第一行代碼》(第三版)- 第 8 章 跨程序共享數據,探究 ContentProvider

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章