Android开发使用到的数据库,官方都已推荐直接使用Room,那我们为何不跟着时代的脚步走呢。可是当需要使用ContentProvider提供跨进程的数据操作时,可能会由于资料不多,会遇到不少的坑,那么这就是本文的目的——方便你我他,快捷使用Room+ContentProvider。
本文主要内容包括如下三个部分:
- Room数据库使用
- ContentProvider服务端
- ContentProvider客户端
———————————————— 下面我们一一道来————————————————
1. 📂 Room数据库使用
1.1 引入Room依赖
在应用中使用 Room,需要将以下依赖项添加到应用的 build.gradle 文件。
dependencies {
def ROOM_VERSION= "2.5.0"
api "androidx.room:room-runtime:$ROOM_VERSION"
kapt "androidx.room:room-compiler:$ROOM_VERSION"
}
注:kapt使用到Kotlin插件,记得在build.gradle 文件顶部添加 plugins { id 'kotlin-kapt' }。另外2.5.0版本的Room需要compileSdk为33。
1.2 创建数据实体
以下代码定义了一个 Note 数据实体。Note 的每个实例都代表应用数据库中 Note 表中的一行。
@Entity
data class Note(
@PrimaryKey var id: String = UUID.randomUUID().toString(), // 记录唯一标识,生成标准确认,uuid
@ColumnInfo(name = "type") var type: Int = -1, // 记录类型:1:语音记录;2:照片记录
@ColumnInfo(name = "time") var time: Long = -1, // 记录生成时间
@ColumnInfo(name = "time_update") var timeUpdate: Long? = -1, // 记录更新时间(暂未使用,此字段为后面业务进行预留)
@ColumnInfo(name = "content") var content: String = "", // 语音流式/手指取词,识别到的文本内容
@ColumnInfo(name = "content_origin") var contentOrigin: String? = "", // 照片识别到的整体文本内容(不显示给用户看,仅做ChatGPT上下文使用)
@ColumnInfo(name = "content_gpt") var contentGpt: String? = "", // ChatGPT洗稿后的文本内容
@ColumnInfo(name = "gpt_context1") var gptContext1: String? = "", // ChatGPT的联想文本1
@ColumnInfo(name = "gpt_context2") var gptContext2: String? = "", // ChatGPT的联想文本2
@ColumnInfo(name = "gpt_context3") var gptContext3: String? = "", // ChatGPT的联想文本3
@ColumnInfo(name = "gpt_explain") var gptExplain: String? = "", // ChatGPT的解释内容
@ColumnInfo(name = "gpt_summary") var gptSummary: String? = "", // ChatGPT的总结文本
@ColumnInfo(name = "img_url") var imgUrl: String? = "", // 照片记录保存的本机图片路径(sdcard路径)
@ColumnInfo(name = "operate") var operate: Int? = -1, // 记录操作:增加(1)、删除(2)、修改(3)
@ColumnInfo(name = "is_sync") var isSync: Boolean? = false, // 是否同步:已同步(true)、未同步(false)
@ColumnInfo(name = "extra1") var extra1: String? = "", // 扩展字段1(暂未使用,此字段为后面业务进行预留)
@ColumnInfo(name = "extra2") var extra2: String? = "", // 扩展字段2(暂未使用,此字段为后面业务进行预留)
) {
companion object {
const val TYPE_VOICE = 1
const val TYPE_CAMERA = 2
}
}
1.3 创建数据访问对象 (DAO)
以下代码定义了一个名为 NoteDao 的 DAO。NoteDao 提供了应用的其余部分用于与 Note 表中的数据交互的方法。
@Dao
interface NoteDao {
@Query("SELECT * FROM Note order by time")
fun getAll(): List<Note>
@Insert
fun insertAll(notes: List<Note>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(note: Note)
@Delete
fun delete(note: Note)
@Update
fun update(note: Note)
}
1.4 创建数据库
以下代码定义了用于保存数据库的 AppDatabase 类。 AppDatabase 定义数据库配置,并作为应用对持久性数据的主要访问点。
数据库类必须满足以下条件:
- 该类必须带有 @Database 注解,该注解包含列出所有与数据库关联的数据实体的 entities 数组;
- 该类必须是一个抽象类,用于扩展 RoomDatabase;
- 对于与数据库关联的每个 DAO 类,数据库类必须定义一个具有零参数的抽象方法,并返回 DAO 类的实例。
@Database(entities = [Note::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
}
1.5 创建数据库帮助类
定义数据实体、DAO 和数据库后,再定义一个数据库帮助类即可。
object RoomDBHelper {
private const val DATABASE_NAME = "mo_notes_db"
private var db: AppDatabase? = null
fun init(appContext: Context) {
if (db == null) {
db = Room.databaseBuilder(appContext, AppDatabase::class.java, DATABASE_NAME).build()
}
}
fun getInstance(): AppDatabase {
if (db == null) init(Utils.getApp())
return db!!
}
}
注:Utils.getApp()方法位于blankj大神奉献的com.blankj.utilcode.util.Utils工具类下 。
1.6 使用Room数据库
使用 AppDatabase 中的抽象方法获取 DAO 的实例,转而可以使用 DAO 实例中的方法与数据库进行交互。
// 建议在Application的onCreate()方法中初始化数据库
RoomDBHelper.init(this)
// 在Model层子线程访问数据库
suspend fun insertAllNotes(notes: List<Note>) = withContext(Dispatchers.IO) {
RoomDBHelper.getInstance().noteDao().insertAll(notes)
}
suspend fun getAllNotes(): List<Note> = withContext(Dispatchers.IO) {
RoomDBHelper.getInstance().noteDao().getAll()
}
2. 🔱 ContentProvider服务端
2.1 实现ContentProvider
接下来咱们康康如何在服务端创建ContentProvider,代码如下:
/**
* Description: 提供给其他进程使用,可对记录进行增删改查。
* CreateDate: 2023/11/7 14:37
* Author: agg
*/
class NotesProvider : ContentProvider() {
private val NOTE_TABLE_NAME = "Note"
override fun onCreate(): Boolean {
context?.let { RoomDBHelper.init(it) }
return true
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val db = getWritableDatabase()
var insertUri: Uri? = null
try {
db.beginTransaction()
values?.let {
it.put("id", UUID.randomUUID().toString())
insertUri = ContentUris.withAppendedId(
uri, db.insert(NOTE_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, it)
)
}
db.setTransactionSuccessful()
} catch (e: Exception) {
e.printStackTrace()
} finally {
db.endTransaction()
}
return insertUri
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
var numRemoved: Int = -1
val db = getWritableDatabase()
try {
db.beginTransaction()
numRemoved = db.delete(NOTE_TABLE_NAME, selection, selectionArgs)
db.setTransactionSuccessful()
} catch (e: Exception) {
e.printStackTrace()
} finally {
db.endTransaction()
}
return numRemoved
}
override fun update(
uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?
): Int {
var numUpdated: Int = -1
val db = getWritableDatabase()
try {
db.beginTransaction()
values?.let {
numUpdated = db.update(
NOTE_TABLE_NAME, SQLiteDatabase.CONFLICT_NONE, it, selection, selectionArgs
)
}
db.setTransactionSuccessful()
} catch (e: Exception) {
e.printStackTrace()
} finally {
db.endTransaction()
}
return numUpdated
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
val db = getReadableDatabase()
var cursor: Cursor? = null
try {
db.beginTransaction()
cursor = db.query("select * from $NOTE_TABLE_NAME")
} catch (e: Exception) {
e.printStackTrace()
} finally {
db.endTransaction()
}
return cursor
}
override fun getType(uri: Uri): String? {
return null
}
private fun getReadableDatabase(): SupportSQLiteDatabase {
return RoomDBHelper.getInstance().openHelper.readableDatabase
}
private fun getWritableDatabase(): SupportSQLiteDatabase {
return RoomDBHelper.getInstance().openHelper.writableDatabase
}
}
注:本文核心在此。
2.2 manifest声明ContentProvider
<provider
android:name=".middleware.provider.NotesProvider"
android:authorities="com.agg.monotes"
android:exported="true"
tools:ignore="ExportedContentProvider" />
3. 💠 ContentProvider客户端
3.1 创建数据实体
客户端可不引入Room数据库,于是客户端可对Note表进行改造如下:
data class Note(
val id: String = "", // 记录唯一标识,生成标准确认,uuid
val type: Int = -1, // 记录类型:1:语音记录;2:照片记录
val time: Long = -1, // 记录生成时间
val timeUpdate: Long = -1, // 记录更新时间(暂未使用,此字段为后面业务进行预留)
val content: String = "", // 语音流式/手指取词,识别到的文本内容
val contentOrigin: String = "", // 照片识别到的整体文本内容(不显示给用户看,仅做ChatGPT上下文使用)
val contentGpt: String = "", // ChatGPT洗稿后的文本内容
val gptContext1: String = "", // ChatGPT的联想文本1
val gptContext2: String = "", // ChatGPT的联想文本2
val gptContext3: String = "", // ChatGPT的联想文本3
val gptExplain: String = "", // ChatGPT的解释内容
val gptSummary: String = "", // ChatGPT的总结文本
val imgUrl: String = "", // 照片记录保存的本机图片路径(sdcard路径)
val operate: Int = -1, // 记录操作:增加(1)、删除(2)、修改(3)
val isSync: Boolean = false, // 是否同步:已同步(true)、未同步(false)
val extra1: String = "", // 扩展字段1(暂未使用,此字段为后面业务进行预留)
val extra2: String = "", // 扩展字段2(暂未使用,此字段为后面业务进行预留)
)
3.2 跨进程操作数据
客户端创建ContentResolver,跨进程操作ContentProvider的数据。
import androidx.core.content.contentValuesOf
class MyResolver(context: Context) {
private var mContentResolver: ContentResolver? = null
private val AUTHORITES = "com.agg.monotes"
init {
mContentResolver = context.contentResolver
}
fun query(): ArrayList<Note> {
val list = ArrayList<Note>()
val cursor =
mContentResolver?.query(Uri.parse("content://$AUTHORITES/Note"), null, null, null, null)
while (cursor?.moveToNext() == true) {
list.add(cursor2Note(cursor))
}
cursor?.close()
return list
}
fun insert(note: Note) {
mContentResolver?.insert(Uri.parse("content://$AUTHORITES/Note"), note2ContentValues(note))
}
private fun cursor2Note(cursor: Cursor): Note = Note(
cursor.getString(0),
cursor.getInt(1),
cursor.getLong(2),
cursor.getLong(3),
cursor.getString(4),
cursor.getString(5),
cursor.getString(6),
cursor.getString(7),
cursor.getString(8),
cursor.getString(9),
cursor.getString(10),
cursor.getString(11),
cursor.getString(12),
cursor.getInt(13),
cursor.getInt(14) > 0,
cursor.getString(15),
cursor.getString(16),
)
private fun note2ContentValues(note: Note): ContentValues = contentValuesOf(
"id" to note.id,
"type" to note.type,
"time" to note.time,
"time_update" to note.timeUpdate,
"content" to note.content,
"content_origin" to note.contentOrigin,
"content_gpt" to note.contentGpt,
"gpt_context1" to note.gptContext1,
"gpt_context2" to note.gptContext2,
"gpt_context3" to note.gptContext3,
"gpt_explain" to note.gptExplain,
"gpt_summary" to note.gptSummary,
"img_url" to note.imgUrl,
"operate" to note.operate,
"is_sync" to note.isSync,
"extra1" to note.extra1,
"extra2" to note.extra2,
)
}
———————————————— 说完了分割线————————————————