Android ContentProvider内容提供者的详解

前面我们已经对四大组件的三个有了一定程度的了解,接下来我们来了解四大组件的最后一个——ContentProvider(内容提供者)。相比较前面的三个,内容提供者在日常的开发过程中用到的比较少,但是这并不意味着它不重要,所以接下来我们就来一起来揭开它神秘的面纱吧。

我们知道在Android中,大部分的数据都是在应用内自己运作的,即应用的数据大部分都是私有的,那么,假设我需要将自己应用内的数据暴露给其他的APP或者说我需要获取系统上的其他应用的数据该怎么办呢?这个时候ContentProvider就派上用场了。

ContentProvider的主要作用就是解决应用之间数据共享的问题,包括获取系统内置的APP(比如联系人)和那些暴露接口的APP,那么ContentProvider到底是怎么运作的呢?在讲解ContentProvider之前,我们先来了解一下几个比较重要的概念。

概念一:URI(统一资源标识符)

我们通常在获取后端数据的时候,都会传递一个URL地址,通常的写法是

*http://www.xxxx.com/web/

然后在此基础上传递参数,通过这样的方式就能拿到我们想要的数据,大家难道都不会好奇为什么就靠这样一行简单的代码就可以拿到我们想要的数据啦,其实大家了解网络这一块的话就会知道,网络上的所有东西,包括网页、文本、图片、音频、视频其实都是有一个地址的,类似于我们的身份证,具有唯一性,每当我们发出一个这样的请求时,系统就会根据我们给的地址去找到对应的资源文件并进行返回,由于地址具有唯一性,所以我们可以正确的拿到准确的数据。那么对于那些手机上的那些资源文件,我们又怎么拿到想要的数据呢?这个时候URI就能派上用场了。那么什么是URI呢?它和URL又有什么区别呢?

URI包括URL和URN两个类别,URL是URI的子集,所以URL一定是URI,而URI不一定是URL。

URI = Uniform Resource Identifier 统一资源标志符,用来标识抽象或物理资源的一个紧凑字符串。

URL = Uniform Resource Locator 统一资源定位符,一种定位资源的主要访问机制的字符串,一个标准的URL必须包括:protocol、host、port、path、parameter、anchor。

URN = Uniform Resource Name 统一资源名称,通过特定命名空间中的唯一名称或ID来标识资源。

当然在Android开发中,我们经常使用的并不是URI而是Uri,那么这个Uri又是什么呢?它和URI有什么区别呢?

· 1.所属的包不同。URI位置在java.net.URI,显然是Java提供的一个类。而Uri位置在android.net.Uri,是由Android提供的一个类。所以初步可以判断,Uri是URI的“扩展”以适应Android系统的需要。

· 2.作用的不同。URI类代表了一个URI(这个URI不是类,而是其本来的意义:通用资源标志符——Uniform Resource Identifier)实例。Uri类是一个不可改变的URI引用,包括一个URI和一些碎片,URI跟在“#”后面。建立并且转换URI引用。而且Uri类对无效的行为不敏感,对于无效的输入没有定义相应的行为,如果没有另外制定,它将返回垃圾而不是抛出一个异常。

简单来说,Uri是Android开发的,扩展了Java中URI的一些功能来特定的适用于Android开发,所以大家在开发时,只使用Android 提供的Uri即可。那么我们来看看Uri的组成结构是什么样子的。

Uri结构:<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]

Scheme:Uri的模式,比如http、file、content。如果Uri中没有指定scheme,意味着Uri无效。

Host:Uri的主机名,比如www.baidu.com。如果Uri中没有指定host,意味着Uri无效。

Port:Uri的端口号,比如8080,仅在scheme和host指定的情况下才有意义。

Path、pathPrefix、pathPattern表示路径信息。Path对应的是完整的路径信息;pathPattern也表示的是完整的路径信息,但是允许其中包含通配符。pathPrefix表示路径的前缀信息。

概念二:UriMatch

主要用于匹配Uri.这里的匹配是发生在ContentProvider中的,假如我们向ContentProvider中插入一条数据,不可能为所欲为的想怎么干就怎么干,在ContentProvider肯定要做一个判断,只有在符合条件下才会去执行你想要执行的操作,这里的判断就是用UriMatch进行匹配。主要的方法有两个:

(1)UriMatch.addURI(String authority, String path, int code)

在ContentProvider添加一个用于匹配的Uri,当匹配成功时返回code。Uri可以是精确的字符串,Uri中带有*表示可匹配任意text,#表示只能匹配数字。

(2)UriMatch.match(Uri uri)

对传递过来的uri进行验证,查看是否匹配成功。

概念三:ContentResolver

主要用于对ContentProvider中的数据进行增删改查的操作,其主要的方法有:

1)public Uri insert(Uri uri, ContentValues values)

该方法用于往ContentProvider添加数据。

2)public int delete(Uri uri, String selection, String[] selectionArgs)

该方法用于从ContentProvider删除数据。

3)public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)

该方法用于更新ContentProvider中的数据。

4)public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

该方法用于从ContentProvider中获取数据。

这四个方法中每个方法的参数所代表的意思是:

(1)Uri uri:指定查询某个应用程序下的某一张表(注意,ContentProvider对外提供的数据更多是以数据库表的形式展现的)

(2)String[] projection:指定查询的列名

(3)ContentValues values:SQLite数据库操作的辅助类

(4)String selection:指定where的约束条件

(5)String[] selectionArgs:为where中的占位符提供具体的值

(6)String sortOrder:指定查询结果的排序方式

概念四:ContentProvider中getType()方法

getType()是所有的内容提供者都必须提供的一个方法,用于获取Uri对象所对应的MIME类型,一个内容URI所对应的MIME字符串主要由3部分组成,Android对这3个部分做了如下格式规定:

(1)必须以vnd开头。

(2)如果内容URI以路径结尾,则后接android.cursor.dir/,如果内容URI以id结尾,则后接android.cursor.item/。

(3)最后接上vnd.<authority>.<path>

讲完了这些重要的概念,接下来我们就来自己创建一个内容提供器。

第一步:创建数据库以及对应的表

/**
 * author: zhoufan
 * data: 2021/6/18 16:42
 * content:
 */
class MyDatabaseHelper(
    context: Context?,
    name: String?,
    factory: SQLiteDatabase.CursorFactory?,
    version: Int
) : SQLiteOpenHelper(context, name, factory, version) {

    private var mContext: Context? = null

    init {
        mContext = context
    }

    // 创建SQL语句
    var CREATE_BOOK =
        "create table Book (" + "id integer primary key autoincrement, " + "author text, " + "price real, " + "pages integer," + "name text)"
    var CREATE_CATEGORY =
        "create table Category (" + "id integer primary key autoincrement, " + "category_name text, " + "category_code integer)"

    override fun onCreate(db: SQLiteDatabase?) {
        db?.execSQL(CREATE_BOOK)
        db?.execSQL(CREATE_CATEGORY)
        Toast.makeText(mContext, "Create succeeded", Toast.LENGTH_SHORT).show()
    }

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

    }
}

第二步:创建一个类继承ContentProvider

/**
 * author: zhoufan
 * data: 2021/6/18 16:10
 * content:自定义内容提供者
 */
class MyProvider : ContentProvider() {

    private val BOOK_DIR = 0
    private val BOOK_ITEM = 1
    private val CATEGORY_DIR = 2
    private val CATEGORY_ITEM = 3
    private val AUTHORITY = "com.steven.sunworld.provider"
    private var uriMatcher: UriMatcher? = null
    private var dbHelper: MyDatabaseHelper? = null

    init {
        uriMatcher = 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)
    }

    /**
     * 初始化的时候调用,通常会在这里完成对数据库的创建和升级
     * 返回true表示创建成功,false表示创建失败
     * 注意只有当存在ContentResolver尝试访问我们程序中的数据时,内容提供器才会被初始化
     */
    override fun onCreate(): Boolean {
        dbHelper = MyDatabaseHelper(context, "BookStore.db", null, 2)
        return true
    }

    /**
     * 从内容提供器中查询数据。
     * uri:查询哪张表
     * projection:查询哪一列
     * selection/selectionArgs:约束查询行
     * sortOrder:对结果进行排序
     */
    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
        val db = dbHelper!!.readableDatabase
        var cursor: Cursor? = null
        when (uriMatcher?.match(uri)) {
            BOOK_DIR -> {
                cursor =
                    db.query("Book", projection, selection, selectionArgs, null, null, sortOrder)
            }
            BOOK_ITEM -> {
                val bookId = uri.pathSegments[1]
                cursor =
                    db.query("Book", projection, "id = ?", arrayOf(bookId), null, null, sortOrder)
            }
            CATEGORY_DIR -> {
                cursor = db.query(
                    "Category",
                    projection,
                    selection,
                    selectionArgs,
                    null,
                    null,
                    sortOrder
                )
            }
            CATEGORY_ITEM -> {
                val categoryId = uri.pathSegments[1]
                cursor = db.query(
                    "Category",
                    projection,
                    "id = ?",
                    arrayOf(categoryId),
                    null,
                    null,
                    sortOrder
                )
            }
        }
        return cursor
    }

    /**
     * 向内容提供者添加一条数据
     * uri:插入到哪张表
     * values:插入的值
     */
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        val db = dbHelper?.writableDatabase
        var uriReturn: Uri? = null
        when (uriMatcher?.match(uri)) {
            BOOK_DIR, BOOK_ITEM -> {
                val newBookId = db?.insert("Book", null, values)
                uriReturn = Uri.parse("content://$AUTHORITY/book/$newBookId")
            }
            CATEGORY_DIR, CATEGORY_ITEM -> {
                val newCategoryId = db?.insert("Category", null, values)
                uriReturn = Uri.parse("content://$AUTHORITY/category/$newCategoryId")
            }
        }
        return uriReturn
    }

    /**
     * 更新内容提供者中已有的数据
     * uri:更新哪一张表的数据
     * values:更新的数据
     * selection/selectionArgs:约束更新行
     */
    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int {
        val db = dbHelper?.writableDatabase
        var updatedRows = 0
        when (uriMatcher!!.match(uri)) {
            BOOK_DIR -> {
                updatedRows = db!!.update("Book", values, selection, selectionArgs)
            }
            BOOK_ITEM -> {
                val bookId = uri.pathSegments[1]
                updatedRows = db!!.update("Book", values, "id=?", arrayOf(bookId))
            }
            CATEGORY_DIR -> {
                updatedRows = db!!.update("Category", values, selection, selectionArgs)
            }
            CATEGORY_ITEM -> {
                val categoryId = uri.pathSegments[1]
                updatedRows = db!!.update("Category", values, "id=?", arrayOf(categoryId))
            }
        }
        return updatedRows
    }

    /**
     * 从内容提供者中删除数据
     * uri:删除哪一张表的数据
     * selection/selectionArgs:约束删除行
     */
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        val db = dbHelper?.writableDatabase
        var deletedRows = 0
        when (uriMatcher!!.match(uri)) {
            BOOK_DIR -> {
                deletedRows = db!!.delete("Book", selection, selectionArgs)
            }
            BOOK_ITEM -> {
                val bookId = uri.pathSegments[1]
                deletedRows = db!!.delete("Book",  "id=?", arrayOf(bookId))
            }
            CATEGORY_DIR -> {
                deletedRows = db!!.delete("Category", selection, selectionArgs)
            }
            CATEGORY_ITEM -> {
                val categoryId = uri.pathSegments[1]
                deletedRows = db!!.delete("Category", "id=?", arrayOf(categoryId))
            }
        }
        return deletedRows
    }

    /**
     * 返回的MIME类型
     */
    override fun getType(uri: Uri): String? {
        var type: String? = null
        when (uriMatcher?.match(uri)) {
            BOOK_DIR -> {
                type =
                    "vnd.android.cursor.dir/vnd.com.steven.sunworld.provider.book"
            }
            BOOK_ITEM -> {
                type =
                    "vnd.android.cursor.item/vnd.com.steven.sunworld.provider.book"
            }
            CATEGORY_DIR -> {
                type =
                    "vnd.android.cursor.dir/vnd.com.steven.sunworld.provider.category"
            }
            CATEGORY_ITEM -> {
                type =
                    "vnd.android.cursor.item/vnd.com.steven.sunworld.provider.category"
            }
        }
        return type
    }
}

第三步:插入数据

fun addData(view: View) {
    val uri = Uri.parse("content://com.steven.sunworld.provider/book")
    val values = ContentValues()
    values.put("name", "A Clash of kings")
    values.put("author", "George Martin")
    values.put("pages", 1040)
    values.put("price", 22.85)
    val newUri = contentResolver.insert(uri, values)
    val newId = newUri!!.pathSegments[1]
    Log.i("zhoufan", newId.toString())
}

第四步:检验结果,查询数据

fun queryData(view: View) {
    val uri = Uri.parse("content://com.steven.sunworld.provider/book")
    val cursor = contentResolver.query(uri, null, null, null, null)
    if (cursor != null) {
        while (cursor.moveToNext()) {
            val name = cursor.getString(cursor.getColumnIndex("name"))
            val author = cursor.getString(cursor.getColumnIndex("author"))
            val pages = cursor.getInt(cursor.getColumnIndex("pages"))
            val price = cursor.getDouble(cursor.getColumnIndex("author"))
            Log.i("zhoufan", name + author + pages + price)
        }
        cursor.close()
    }
}

最后别忘记在清单文件里面注册我们自己定义的ContentProvider

<provider
    android:name=".contentprovider.MyProvider"
    android:authorities="com.steven.sunworld.provider"
    android:enabled="true" 
    android:exported="true" />

到这里,自定义的ContentProvider就完成了,需要注意的是authorities在全局里面要保持一致,一般情况下都是以包名+provider来定义。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容