当前位置: 首页 > news >正文

「解锁 Android 数据共享新姿势!ContentProvider 跨应用交互全攻略」

「解锁 Android 数据共享新姿势!ContentProvider 跨应用交互全攻略」

前言:为什么需要 ContentProvider

想象一下这个场景:

┌─────────────┐ ┌─────────────┐ │ 短信应用 │ │ 备份应用 │ │ (数据源) │ │ (使用者) │ └──────┬──────┘ └──────┬──────┘ │ │ │ content://sms/ │ │◀────────────────────────────────▶ │ ContentResolver │

如果不用 ContentProvider,备份应用想读取短信数据?要么短信应用开放数据库权限(危险!),要么每个应用都自己实现一套数据共享方案(重复造轮子)。

ContentProvider 就是 Android 提供的标准化数据共享方案!


一、ContentProvider 核心概念

1. 三个关键角色

角色作用代码位置
ContentProvider数据提供者,定义 CRUD 接口服务端
ContentResolver数据解析器,客户端调用接口客户端
URI唯一标识数据的位置两者都用

2. URI 格式

content://<authority>/<path>/<id>
部分示例值说明
content://前缀固定格式
authoritycom.example.provider提供者唯一标识
pathnotes表/数据集路径
id1可选,特定记录

示例:

  • 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/1

2. 查询数据

// 查询所有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/文件/网络,对外都是表的形式
http://www.cnnetsun.cn/news/2049735.html

相关文章:

  • 保姆级教程:在Mac M1/M2芯片上手动安装PyTorch全家桶(附版本对照表)
  • 三步搞定Windows网络测速:iperf3-win-builds终极指南
  • ESP32开发终极指南:从零开始构建物联网项目的完整教程
  • [特殊字符] 斜齿轮时变啮合刚度计算模型:基于势能法与切片法的 MATLAB 实现
  • 别只换电阻了!从失效数据看,线绕电阻90%的故障都是这个原因
  • 告别LabVIEW!用Python+PyVISA搞定示波器自动化(附Keysight IO库配置避坑指南)
  • 红菱vs焦玉芹,被张逗逗饰演的两个“坏女人”惊到了!
  • 【C++26合约编程权威指南】:20年专家亲授生产级契约建模、编译器适配与错误拦截实战
  • VSCode金融编码环境安全升级指南:2026新版TLS 1.3+静态扫描+密钥隔离三重防护落地手册
  • 英雄联盟Akari助手:3分钟快速上手的终极游戏辅助工具包
  • Yolo 多任务推理,摄像头+视频实时推理,实现关键点、分割、检测等模型推理部署
  • Linux 系统编程 进程篇(五)
  • 2026年五款降AI工具维普检测效果横评:同篇文章全程实测记录
  • 什么是“商机发现算法”?招标采购导航网用一篇文章讲清楚
  • 中小企业短期靠外包,长期必须培养懂业务的AI核心人才。
  • Jellyfin Kodi插件:从技术原理到实战应用的全方位解析
  • 3步搞定跨平台B站视频下载:从零开始构建个人视频资源库
  • Ceph运维日常:这20个高频命令,帮你快速定位集群状态与故障
  • 别再死记硬背了!三菱FX3U的ADD、MUL指令,用这几个真实项目案例一讲就懂
  • 从CD4051到现代模拟开关:40年芯片演进史与选型避坑指南
  • 在树莓派4B上从零部署ROS2 Humble:实测小鱼fishros脚本的保姆级教程与性能对比
  • 古兰经-NLP研究数据集-完整经文文本-多语言翻译-详细注释-应用场景丰富
  • Arduino IDE 1.x终极指南:三步开启你的硬件编程之旅
  • 3分钟让Windows任务栏变身macOS风格:TaskbarX美化全攻略
  • ROS Melodic下,如何用MetaMemoryT修改版Robotiq包快速搞定Gazebo仿真(含UR5整合)
  • Cursor Free VIP破解工具:三步轻松绕过试用限制的终极指南
  • Docker 27容器网络隔离失效导致订单重复提交(2024年某券商真实故障复盘)
  • 【LeetHOT100】回文链表——Java多解法详解
  • HGVE-2025-E009
  • 测试人员日常工作