Android Room简单使用,结合Flow监听数据变化

4,254 阅读3分钟

Room 持久性库在 SQLite 的基础上提供了一个抽象层,让用户能够在充分利用 SQLite 的强大功能的同时,获享更强健的数据库访问机制。

该库可帮助您在运行应用的设备上创建应用数据的缓存。此缓存充当应用的单一可信来源,使用户能够在应用中查看关键信息的一致副本,无论用户是否具有互联网连接。

Room特点

  • 主流框架
  • 支持SQL语句
  • 操作数据库需要编写抽象函数,操作简便
  • 官方维护, JetPack组件中的数据库框架
  • 监听数据库
  • 支持嵌套对象
  • 支持Kotlin 协程/RxJava
  • 具备SQL语句高亮和编译期检查(具备AndroidStudio的支持)
  • 使用SQLite便于多个平台的数据库文件传递(例如有些联系人信息就是一个SQLite文件)
  • 由于是SQLite可以通过第三方框架进行数据库加密(ROOM原生不支持)
  • 可以配合AndroidStudio自带的数据库查看工具窗口

总结起来,简单入门,功能强大,数据库监听,支持Kotlin协程、RxJava、Flow、Guava。

Room结构

Room 中有 3 个主要组件。

  • Database:此注解将类标记为数据库。它应该是一个扩展的抽象类RoomDatabaseRoom.databaseBuilder在运行时,您可以通过或获取它的一个实例Room.inMemoryDatabaseBuilder

    数据库类定义了数据库中实体和数据访问对象的列表。它也是底层连接的主要访问点。

  • Entity:此注解将类标记为数据库行。对于每个Entity,都会创建一个数据库表来保存项目。实体类必须在Database#entities数组中被引用。实体(及其超类)的每个字段都保存在数据库中,除非另有说明(有关Entity详细信息,请参阅文档)。

  • Dao:此注解将类或接口标记为数据访问对象。数据访问对象是 Room 的主要组件,负责定义访问数据库的方法。被注解的类Database必须有一个抽象方法,该方法有 0 个参数并返回被 Dao 注解的类。在编译时生成代码时,Room 将生成此类的实现。

    使用 Dao 类进行数据库访问而不是查询构建器或直接查询允许您在不同组件之间保持分离,并在测试您的应用程序时轻松模拟数据库访问。 image.png

gradle添加依赖库

dependencies {
    //可选 - kotlin 协程,含有Flow
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'

    def room_version = "2.2.3"
    
    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version" // Kotlin 使用 kapt 替代 annotationProcessor

    // 可选 - Kotlin扩展和协程支持
    implementation "androidx.room:room-ktx:$room_version"

    // 可选 - RxJava 支持
    implementation "androidx.room:room-rxjava2:$room_version"

    // 可选 - Guava 支持, including Optional and ListenableFuture
    implementation "androidx.room:room-guava:$room_version"

    // 测试
    testImplementation "androidx.room:room-testing:$room_version"

}

Gradle配置


    javaCompileOptions {
        annotationProcessorOptions {
            arguments = [
                    "room.schemaLocation":"$projectDir/schemas".toString(),
                    "room.incremental":"true",
                    "room.expandProjection":"true"]
        }
    }
 }
  • room.expandProjection 使用星投影时会根据函数返回类型来重写SQL查询语句
  • room.schemaLocation 输出数据库概要,可以查看字段信息,版本号,数据库创建语句等
  • room.incremental 启用Gradle增量注释处理器

使用

首先创建Entity类

通过@Entity标记创建表格,数据库初始化时自动生成表,我们可以通过该类定义对应的字段属性

@Entity(tableName = "user")//表名默认为UserEntity类名,使用tableName可重命名
data class UserEntity(
    @PrimaryKey //主键
    var code: Int?,

    @ColumnInfo(name = "user_name")//列名默认为字段名name,可使用该注解重命名
    var name:String?,
    var age:Int?,
    var grender:String?,
)

创建Dao类

创建接口类UserDao,使用@Dao标记,在该接口类中我们定义增删改查的操作。

@Dao
interface UserFlowDao {

    @Query("select * from user")//结合flow监听数据变化,避免重复更新ui数据
    fun queryAllUser(): Flow<List<UserEntity>>

    //key键重复的替换
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(user: UserEntity)

    @Query("delete from user")//删除所有数据
    fun deleteAll()

    @Query("select * from user WHERE age = :age")//根据条件查询
    fun queryAgeUser(age: Int): List<UserEntity>

    @Query("delete from user WHERE code = :code")
    fun deletedUser(code: Int)
}

操作数据库需要在子线程中进行,否则会报异常java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

Query查询操作,执行SQL语句时,数据表严格对照类名(区分大小写),否则会影响重命名类名时无法照顾到SQL语句中的表名。

创建数据库

创建database抽象类,继承RoomDatabase,把所有Entity的class对象联系在一起,组成完整的数据库。通过@Database标记该抽象类,定义获取dao接口的抽象方法

@Database(entities = [UserEntity::class], version = 1, exportSchema = false)
abstract class SQLDatabase :RoomDatabase(){
    abstract fun userFlow():UserFlowDao //flow操作类
}

参数

  • entities 传入Entity类class对象,也是所有表格
  • version 数据库版本号
  • exportSchem 设置是否导出数据库schem,默认为true,需要在build.gradle中设置

Demo实例

在的activity或fragment中,room提供databaseBuilder()方法初始化

private val database by lazy {
    Room.databaseBuilder(this, SQLDatabase::class.java, "test_demo").build()
}

获取该数据库对象,有了database我们就可以拿到Dao的对象实例,通过这个可以进行数据库的增删改查操作。

第一个参数context上下文,传入当前this即可。

第二个参数class,传入room的database类class对象,也就是@Database标记的类。

第三个参数name,数据库保存后的文件名称。

UI界面布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="20dp"
        android:paddingRight="20dp"
        android:paddingTop="10dp"
        android:layout_marginTop="20dp"
        android:layout_gravity="center_vertical"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/tv_add"
            android:layout_width="wrap_content"
            android:layout_height="30dp"
            android:paddingRight="5dp"
            android:paddingLeft="5dp"
            android:text="增加数据"
            android:gravity="center"
            android:textColor="@color/white"
            android:layout_gravity="center_horizontal"
            android:background="@color/design_default_color_secondary"/>


        <TextView
            android:id="@+id/tv_deletedAll"
            android:layout_width="wrap_content"
            android:layout_height="30dp"
            android:paddingRight="5dp"
            android:paddingLeft="5dp"
            android:layout_marginLeft="10dp"
            android:text="删除全部"
            android:gravity="center"
            android:textColor="@color/white"
            android:layout_gravity="center_horizontal"
            android:background="@color/design_default_color_secondary"/>

        <TextView
            android:id="@+id/tv_selectQuery"
            android:layout_width="wrap_content"
            android:layout_height="30dp"
            android:paddingRight="5dp"
            android:paddingLeft="5dp"
            android:layout_marginLeft="10dp"
            android:text="条件查询"
            android:gravity="center"
            android:textColor="@color/white"
            android:layout_gravity="center_horizontal"
            android:background="@color/design_default_color_secondary"/>


    </LinearLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_marginTop="10dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>


</LinearLayout>

编写Activity

class UserDaoDemoActivity : AppCompatActivity(), CoroutineScope by MainScope() {

    //数据实例对象,可以初始化一次即可
    private val database by lazy {
        Room.databaseBuilder(this, SQLDatabase::class.java, "test_demo").build()
    }

    private var mUserFlowDao: UserFlowDao? = null

    private var mAdapter: UserAdapter? = null

    private var recyclerView: RecyclerView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_user_dao)

        mUserFlowDao = database.userFlow()

        initView()
    }

    private fun initView() {

        recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        recyclerView!!.layoutManager = LinearLayoutManager(this)

        mAdapter = UserAdapter(this)
        recyclerView!!.adapter = mAdapter

        var index = 0
        //增加数据
        findViewById<TextView>(R.id.tv_add).setOnClickListener {
            launch {
                withContext(Dispatchers.IO) {//操作数据库必须要在子线程中操作,通过协程使用io线程执行增加数据的操作
                    index++
                    val randValue = (Math.random() * 900).toInt() + 10
                    mUserFlowDao!!.insert(UserEntity(randValue, "学生$randValue", 10 + index, "男"))
                }
            }
        }

        //删除所有
        findViewById<TextView>(R.id.tv_deletedAll).setOnClickListener {
            launch {
                withContext(Dispatchers.IO) {
                    mUserFlowDao!!.deleteAll()
                }
            }
        }

        findViewById<TextView>(R.id.tv_selectQuery).setOnClickListener {
            launch {//条件查询
                withContext(Dispatchers.IO) {
                    val list = mUserFlowDao!!.queryAgeUser(20)

                    withContext(Dispatchers.Main) {//查询的结果在主线程更新数据
                        mAdapter!!.setData(list)
                    }
                }
            }
        }

        mAdapter!!.setOnItemClickListener(object : UserAdapter.OnItemClickListener {
            override fun onDeleted(userEntity: UserEntity) {
                launch {
                    withContext(Dispatchers.IO) {
                        mUserFlowDao!!.deletedUser(userEntity.code!!)
                    }
                    Toast.makeText(this@UserDaoDemoActivity, "删除成功", Toast.LENGTH_SHORT).show()
                }
            }

        })
//结合flow监听数据库变化,我们只需要注册一次即可,当执行增、删操作后会自动执行数据查询,更新UI。否则每次执行后我们都要重新查询数据再更新UI
        updateView()
    }

    private fun updateView() {
        mUserFlowDao!!.queryAllUser().onEach {
            mAdapter!!.setData(it)
        }.launchIn(this)

    }
}

Adapter代码

class UserAdapter(var mContext: Context) :
    RecyclerView.Adapter<UserAdapter.ViewHolder>() {


    private var mData: List<UserEntity>? = null

    fun setData(data: List<UserEntity>?) {
        this.mData = data
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {

        return ViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item_user, parent, false))
    }

    override fun getItemCount() = if (mData.isNullOrEmpty()) 0 else mData!!.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val userEntity = mData!!.get(position)
        holder.tvContent!!.text =
            "${userEntity.name}  学生号:${userEntity.code}   年龄:${userEntity.age}  性别:${userEntity.grender}"

        holder.tvContent!!.setOnClickListener {
            if (listener != null)
                listener!!.onDeleted(mData!![position])
        }
    }

    class ViewHolder : RecyclerView.ViewHolder {

        var tvContent: TextView? = null

        constructor(itemView: View) : super(itemView) {
            tvContent = itemView.findViewById<TextView>(R.id.tv_content)
        }
    }

    private var listener: OnItemClickListener? = null

    fun setOnItemClickListener(listener: OnItemClickListener?) {
        this.listener = listener
    }

    interface OnItemClickListener {
        fun onDeleted(userEntity: UserEntity)
    }
}

运行APP

zv92x-xbtbf.gif

我们也可以使用android studio提供的工具查看数据库实时数据,Android studio Electric Eel版本可以在App Inspection查看,如下图:

image.png