文章目錄
簡介
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 時傳入的前兩個參數這樣拼接出來的 URIcontent://$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_NAME
、Phone.NUMBER
這兩列,最後按照 Phone.DISPLAY_NAME
排序。運行程序,輸出和剛纔一樣。
這就是通過 ContentProvider 讀取系統通訊錄的方法,不過要想對系統通訊錄進行增刪改,和我們自定義的 ContentProvider 有點出入的,因爲通訊錄涉及多個表,所以必須同時修改多個表才行,感興趣的讀者可以自行查閱文檔瞭解。
以上就是 ContentProvider 的使用方式,至此,我們已將 Android 四大組件都梳理了一遍,對其他組件感興趣的讀者可以訪問本專欄的其他文章查看。