「解锁 Android 数据共享新姿势!ContentProvider 跨应用交互全攻略」
「解锁 Android 数据共享新姿势!ContentProvider 跨应用交互全攻略」
前言:为什么需要 ContentProvider
想象一下这个场景:
┌─────────────┐ ┌─────────────┐ │ 短信应用 │ │ 备份应用 │ │ (数据源) │ │ (使用者) │ └──────┬──────┘ └──────┬──────┘ │ │ │ content://sms/ │ │◀────────────────────────────────▶ │ ContentResolver │如果不用 ContentProvider,备份应用想读取短信数据?要么短信应用开放数据库权限(危险!),要么每个应用都自己实现一套数据共享方案(重复造轮子)。
ContentProvider 就是 Android 提供的标准化数据共享方案!
一、ContentProvider 核心概念
1. 三个关键角色
| 角色 | 作用 | 代码位置 |
|---|---|---|
| ContentProvider | 数据提供者,定义 CRUD 接口 | 服务端 |
| ContentResolver | 数据解析器,客户端调用接口 | 客户端 |
| URI | 唯一标识数据的位置 | 两者都用 |
2. URI 格式
content://<authority>/<path>/<id>| 部分 | 示例值 | 说明 |
|---|---|---|
content:// | 前缀 | 固定格式 |
authority | com.example.provider | 提供者唯一标识 |
path | notes | 表/数据集路径 |
id | 1 | 可选,特定记录 |
示例:
content://com.example.provider/notes- 获取所有便签content://com.example.provider/notes/1- 获取 ID 为 1 的便签
二、实战:手写一个 NotesProvider
1. 定义数据模型
// Note.ktdataclassNote(valid:Long=0,valtitle:String,valcontent:String,valcreatedAt:Long=System.currentTimeMillis(),valupdatedAt:Long=System.currentTimeMillis())2. 定义 Contract(契约类)
// NotesContract.ktobjectNotesContract{constvalAUTHORITY="com.example.activitylifecycletest.provider"valBASE_CONTENT_URI:Uri=Uri.parse("content://$AUTHORITY")objectNotes{constvalTABLE_NAME="notes"constvalCONTENT_PATH="notes"valCONTENT_URI:Uri=BASE_CONTENT_URI.buildUpon().appendPath(CONTENT_PATH).build()// MIME 类型constvalCONTENT_TYPE="vnd.android.cursor.dir/vnd.$AUTHORITY.$TABLE_NAME"constvalCONTENT_ITEM_TYPE="vnd.android.cursor.item/vnd.$AUTHORITY.$TABLE_NAME"objectColumns{constval_ID=BaseColumns._IDconstvalTITLE="title"constvalCONTENT="content"constvalCREATED_AT="created_at"constvalUPDATED_AT="updated_at"}}funbuildNoteUri(id:Long):Uri=Uri.withAppendedPath(Notes.CONTENT_URI,id.toString())}契约类的优势:
- 集中管理常量,避免硬编码字符串
- 供其他应用引用,保证一致性
- IDE 自动补全,减少错误
3. 实现数据库帮助类
// NotesDbHelper.ktclassNotesDbHelper(context:Context):SQLiteOpenHelper(context,"notes_database.db",null,1){overridefunonCreate(db:SQLiteDatabase){db.execSQL(""" CREATE TABLE notes ( _id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ) """)}overridefunonUpgrade(db:SQLiteDatabase,oldVersion:Int,newVersion:Int){db.execSQL("DROP TABLE IF EXISTS notes")onCreate(db)}}4. 实现 ContentProvider(核心)
// NotesProvider.ktclassNotesProvider:ContentProvider(){privatelateinitvardbHelper:NotesDbHelpercompanionobject{privateconstvalCODE_NOTES=1// 匹配 content://.../notesprivateconstvalCODE_NOTE_ID=2// 匹配 content://.../notes/1privatevaluriMatcher=UriMatcher(UriMatcher.NO_MATCH).apply{addURI(NotesContract.AUTHORITY,"notes",CODE_NOTES)addURI(NotesContract.AUTHORITY,"notes/#",CODE_NOTE_ID)}}overridefunonCreate():Boolean{dbHelper=NotesDbHelper(context!!)returntrue}overridefunquery(uri:Uri,projection:Array<outString>?,selection:String?,selectionArgs:Array<outString>?,sortOrder:String?):Cursor?{valdb=dbHelper.readableDatabasevalcursor=when(uriMatcher.match(uri)){CODE_NOTES->db.query(NotesContract.Notes.TABLE_NAME,projection,selection,selectionArgs,null,null,sortOrder?:"${NotesContract.Notes.Columns.CREATED_AT}DESC")CODE_NOTE_ID->{valid=uri.lastPathSegment// 从 URI 提取 IDdb.query(NotesContract.Notes.TABLE_NAME,projection,"${NotesContract.Notes.Columns._ID}= ?",arrayOf(id),null,null,null)}else->throwIllegalArgumentException("Unknown URI:$uri")}cursor?.setNotificationUri(context?.contentResolver,uri)returncursor}overridefuninsert(uri:Uri,values:ContentValues?):Uri?{if(uriMatcher.match(uri)!=CODE_NOTES){throwIllegalArgumentException("Invalid URI:$uri")}valdb=dbHelper.writableDatabasevalid=db.insert(NotesContract.Notes.TABLE_NAME,null,values)if(id>0){valnewUri=ContentUris.withAppendedId(NotesContract.Notes.CONTENT_URI,id)context?.contentResolver?.notifyChange(newUri,null)returnnewUri}returnnull}overridefunupdate(uri:Uri,values:ContentValues?,selection:String?,selectionArgs:Array<outString>?):Int{valdb=dbHelper.writableDatabasevalrows=when(uriMatcher.match(uri)){CODE_NOTES->db.update(NotesContract.Notes.TABLE_NAME,values,selection,selectionArgs)CODE_NOTE_ID->{valid=uri.lastPathSegment db.update(NotesContract.Notes.TABLE_NAME,values,"${NotesContract.Notes.Columns._ID}= ?",arrayOf(id))}else->throwIllegalArgumentException("Unknown URI:$uri")}if(rows>0)context?.contentResolver?.notifyChange(uri,null)returnrows}overridefundelete(uri:Uri,selection:String?,selectionArgs:Array<outString>?):Int{valdb=dbHelper.writableDatabasevalrows=when(uriMatcher.match(uri)){CODE_NOTES->db.delete(NotesContract.Notes.TABLE_NAME,selection,selectionArgs)CODE_NOTE_ID->{valid=uri.lastPathSegment db.delete(NotesContract.Notes.TABLE_NAME,"${NotesContract.Notes.Columns._ID}= ?",arrayOf(id))}else->throwIllegalArgumentException("Unknown URI:$uri")}if(rows>0)context?.contentResolver?.notifyChange(uri,null)returnrows}overridefungetType(uri:Uri):String{returnwhen(uriMatcher.match(uri)){CODE_NOTES->NotesContract.Notes.CONTENT_TYPE CODE_NOTE_ID->NotesContract.Notes.CONTENT_ITEM_TYPEelse->throwIllegalArgumentException("Unknown URI:$uri")}}}5. 在 AndroidManifest 注册
<providerandroid:name=".NotesProvider"android:authorities="com.example.activitylifecycletest.provider"android:exported="false"/>三、客户端调用方式
1. 插入数据
valvalues=contentValuesOf("title"to"学习 Kotlin","content"to"高阶函数真香!","created_at"toSystem.currentTimeMillis(),"updated_at"toSystem.currentTimeMillis())valuri=contentResolver.insert(NotesContract.Notes.CONTENT_URI,values)// uri = content://com.example.../notes/12. 查询数据
// 查询所有valcursor=contentResolver.query(NotesContract.Notes.CONTENT_URI,null,// projectionnull,// selectionnull,// selectionArgs"${NotesContract.Notes.Columns.CREATED_AT}DESC")cursor?.use{while(it.moveToNext()){valid=it.getLong(it.getColumnIndexOrThrow("_id"))valtitle=it.getString(it.getColumnIndexOrThrow("title"))valcontent=it.getStringOrNull(it.getColumnIndexOrThrow("content"))}}// 按 ID 查询valuri=NotesContract.buildNoteUri(1)valcursor=contentResolver.query(uri,null,null,null,null)3. 更新数据
valuri=NotesContract.buildNoteUri(1)valvalues=contentValuesOf("title"to"更新后的标题","updated_at"toSystem.currentTimeMillis())valrows=contentResolver.update(uri,values,null,null)4. 删除数据
valuri=NotesContract.buildNoteUri(1)valrows=contentResolver.delete(uri,null,null)四、UriMatcher 匹配码详解
uriMatcher.match(uri) 返回值: CODE_NOTES = 1 → content://com.example.provider/notes CODE_NOTE_ID = 2 → content://com.example.provider/notes/1 CODE_NOTE_ID = 2 → content://com.example.provider/notes/123 NO_MATCH (-1) → 任何不匹配的 URI五、MIME 类型说明
| 类型 | 格式 | 用于 |
|---|---|---|
vnd.android.cursor.dir/vnd... | 目录类型 | 多个数据content://.../notes |
vnd.android.cursor.item/vnd... | 单项类型 | 单条数据content://.../notes/1 |
六、数据变化监听
// 注册观察者valobserver=object:ContentObserver(Handler(Looper.getMainLooper())){overridefunonChange(selfChange:Boolean){Log.d("DataChanged","数据发生了变化!")}}contentResolver.registerContentObserver(NotesContract.Notes.CONTENT_URI,true// 是否监听子目录)// 取消观察contentResolver.unregisterContentObserver(observer)七、权限控制
<!-- AndroidManifest.xml --><providerandroid:name=".NotesProvider"android:authorities="com.example.provider"android:permission="com.example.provider.READ_WRITE"/><!-- 其他应用需要申请权限 --><uses-permissionandroid:name="com.example.provider.READ_WRITE"/>八、什么时候用 ContentProvider
| 场景 | 是否需要 |
|---|---|
| 数据只在当前应用使用 | 不需要 |
| 需要给其他应用提供数据 | 必须用 |
| 需要使用 CursorLoader | 必须用 |
| 需要数据同步到云端 | 必须用 |
| 访问系统自带的数据(联系人、短信等) | 不需要,自己有 Provider |
总结
┌─────────────────────────────────────────────────────┐ │ ContentProvider │ ├─────────────────────────────────────────────────────┤ │ 1. 定义 Contract → 统一 URI 和列名 │ │ 2. 实现 Provider → CRUD + UriMatcher + getType │ │ 3. 注册 Provider → AndroidManifest │ │ 4. 客户端调用 → ContentResolver │ └─────────────────────────────────────────────────────┘核心要点:
- ContentProvider = 数据共享的标准接口
- UriMatcher = 路由分发(根据 URI 决定操作)
- ContentResolver = 客户端调用代理
- 一切皆表:无论底层是 SQLite/文件/网络,对外都是表的形式
