Jetpack Flow 、Room 初学者学习记录

news2025/2/27 10:24:11

学习使用响应式Flow操作数据,记录自己学习的过程。
ContactViewModel 是一个 ViewModel,它依赖于一个Room操作接口 ContactDao ,访问对象来获取联系人数据。它使用了 StateFlow 来处理状态的变化和数据的更新。ViewModels 通常用于管理应用的状态,当状态发生变化时,ViewModels 负责更新这些状态,然后通过 StateFlow 将这些变化传递给 UI。使用 state.update() 是为了允许外部观察者(如 Activity 或 Fragment)能够观察到状态的变化。

源码地址
B站视频地址 Android Jetpack Room 初学者完整指南

加入Room依赖:
plugins {
id ‘com.android.application’
id ‘org.jetbrains.kotlin.android’
id ‘kotlin-kapt’
}
// Room
implementation “androidx.room:room-ktx:2.5.0”
kapt “androidx.room:room-compiler:2.5.0” //room 注释处理

建立Room数据库,第一步,定义联系人数据库的表,包含姓、名和电话。Contact是实体类,对应的是数据库的一张表结构。需要使用注解 @Entity 标记 。其中电话号码是String而不是Int,因为在点击对话框AlertDialog,输入框中添加手机号码,是String类型。

@Entity
data class Contact(
    val firstName: String,
    val lastName: String,
    val phoneNumber: String,
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0
)

第二步,创建ContactDao:包含访问一系列访问数据库的方法,如插入、删除、以…方式排序联系人。需要使用注解 @Dao 标记。这里使用协程来操作数据库,三个查询函数返回Flow<List>。Room的数据一旦变化可以被观察到,数据源将发送到Flow中传输,那么我们已经得到的这个流将发出一个新的联系人列表,其中包括新添加的联系人,通过collect显示到UI中。 Room中修改的数据是水源,通过水管flow,传送到接收端。

@Dao
interface ContactDao {

    @Upsert
    suspend fun upsertContact(contact: Contact)

    @Delete
    suspend fun deleteContact(contact: Contact)

    @Query("SELECT * FROM contact ORDER BY firstName ASC")
    fun getContactsOrderedByFirstName(): Flow<List<Contact>>

    @Query("SELECT * FROM contact ORDER BY lastName ASC")
    fun getContactsOrderedByLastName(): Flow<List<Contact>>

    @Query("SELECT * FROM contact ORDER BY phoneNumber ASC")
    fun getContactsOrderedByPhoneNumber(): Flow<List<Contact>>
}

第三步,创建ContactDatabase:数据库持有者,作为与应用持久化相关数据的底层连接的主要接入点。需要使用注解 @Database 标记。 使用@Database注解需满足3个条件: 1)定义的类必须是一个继承于RoomDatabase的抽象类。 2)在注解中需要定义与数据库相关联的实体类列表,如[Contact::class]。 3)包含一个没有参数的抽象方法或属性并且返回一个带有注解的 @Dao。

@Database(entities = [Contact::class], version = 1, exportSchema = false)
abstract class ContactDatabase : RoomDatabase() {
    abstract val dao: ContactDao
}

enum类中建立一个排序类型:

enum class SortType {
    FIRST_NAME,
    LAST_NAME,
    PHONE_NUMBER
}
建立用户与UI的交互点击事件,先在FloatingActionButton点击后,执行操作添加、删除、保存、排序联系人。

这里使用sealed interface,它是一个限制性的类层次结构,在编译时就知道一个密封接口有哪些可能的子类型,检查是否覆盖了所有可能的情况,就不会遗漏某些分支,如此可以更好地控制继承关系,避免出现意外的子类型。同时还能够定义更多样化的子类型,如使用数据类(data class),对象(object),普通类(class),或者另一个密封类(sealed class)作为子类型。
在ContactViewModel 中when分支处理ContactEvent,add remaining branches,可以显示每一个点击事件。

sealed interface ContactEvent {
    object SaveContact: ContactEvent
    data class SetFirstName(val firstName: String): ContactEvent
    data class SetLastName(val lastName: String): ContactEvent
    data class SetPhoneNumber(val phoneNumber: String): ContactEvent
    object ShowDialog: ContactEvent
    object HideDialog: ContactEvent
    data class SortContacts(val sortType: SortType): ContactEvent
    data class DeleteContact(val contact: Contact): ContactEvent
}

创建联系人时,临时存储联系人的状态。

data class ContactState(
    var contacts: List<Contact> = emptyList(),
    var firstName: String = "",
    var lastName: String = "",
    var phoneNumber: String  ="",
    var isAddingContact:Boolean = false,
    var sortType: SortType = SortType.FIRSTNAME
)

ContactViewModel中定义了三个状态流: _state、_sortType和_contact,并使用combine函数将它们组合在一起,一旦其中某个流变化将会更新state这个冷流。定义了一个onEvent函数来处理ContactEvent事件。
_state.contacts一直是一个空列表,其中没有联系人列表。state.contacts显示最新的联系人列表,而 _contacts只有用户点击排序按钮或保存联系人后,从数据库获取相应的联系人列表。_state在用户添加或保存联系人、输入联系人的姓、名或电话,_state流都将变化。_sortType在点击不同排序选项时将会变化,flatMapLatest获取最新排序产生的联系人数据。
在转换成stateFlow时,使用 WhileSubscribed(5000),当最后一个收集者消失后再保持上游数据流活跃状态 5 秒钟。这样在某些特定情况 (如短时退出到桌面,旋转屏幕) 下可以避免重启上游数据流。当上游数据流的创建成本很高,或者在 ViewModel 中使用这些操作符时,这一技巧不会让数据丢失。
每次_state改变都使用data类的浅拷贝,这是一种方便的方式来创建一个新的数据类实例,同时保留原始对象的属性值。在 MutableStateFlow 中,update 是一个用于原子性更新状态值的函数。它允许以一种线程安全的方式修改状态流的值,并且提供了对当前值的访问。如果直接赋值给StateFlow对象,例如_state.value.lastName = event.lastName,Flow框架不会检测到状态的变化,因为直接赋值操作并没有通过update函数来更新状态。这意味着任何依赖于_state的状态或值都不会被自动更新,可能会导致不一致或错误的结果。

@OptIn(ExperimentalCoroutinesApi::class)
class ContactViewModel(
    private val dao: ContactDao
): ViewModel() {

    private val _sortType = MutableStateFlow(SortType.FIRST_NAME)
    private val _contacts = _sortType
        .flatMapLatest { sortType ->
            when(sortType) {
                SortType.FIRST_NAME -> dao.getContactsOrderedByFirstName()
                SortType.LAST_NAME -> dao.getContactsOrderedByLastName()
                SortType.PHONE_NUMBER -> dao.getContactsOrderedByPhoneNumber()
            }
        }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())

    private val _state = MutableStateFlow(ContactState())
    //_state 是 MutableStateFlow ,_sortType 是 MutableStateFlow<SortType>,_contacts是 StateFlow<List<Contact>>,
    val state = combine(_state, _sortType, _contacts) { state, sortType, contacts ->  //值参state是 ContactState类,sortType是SortType,_contacts 是List<Contact>,
        state.copy(
            contacts = contacts,
            sortType = sortType
        )
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ContactState())

    fun onEvent(event: ContactEvent) {
        when(event) {
            is ContactEvent.DeleteContact -> {
                viewModelScope.launch {
                    dao.deleteContact(event.contact)
                }
            }
            ContactEvent.HideDialog -> {  // update是MutableStateFlow的内联函数 ,public inline fun <T> MutableStateFlow<T>.update(function: (T) -> T ): Unit
                _state.update { it.copy(
                    isAddingContact = false
                ) }
            }
            ContactEvent.SaveContact -> {
                val firstName = state.value.firstName
                val lastName = state.value.lastName
                val phoneNumber = state.value.phoneNumber

                if(firstName.isBlank() || lastName.isBlank() || phoneNumber.isBlank()) {
                    return
                }

                val contact = Contact(
                    firstName = firstName,
                    lastName = lastName,
                    phoneNumber = phoneNumber
                )
                viewModelScope.launch {
                    dao.upsertContact(contact)
                }
                _state.update { it.copy( //完成添加联系人后重置当前联系人为初始状态
                    isAddingContact = false,
                    firstName = "",
                    lastName = "",
                    phoneNumber = ""
                ) }
            }
            is ContactEvent.SetFirstName -> {
                _state.update { it.copy(
                    firstName = event.firstName
                ) }
            }
            is ContactEvent.SetLastName -> {
                _state.update { it.copy(
                    lastName = event.lastName
                ) }
            }
            is ContactEvent.SetPhoneNumber -> {
                _state.update { it.copy(
                    phoneNumber = event.phoneNumber
                ) }
            }
            ContactEvent.ShowDialog -> {
                _state.update { it.copy(
                    isAddingContact = true
                ) }
            }
            is ContactEvent.SortContacts -> {
                _sortType.value = event.sortType
            }
        }
    }
}

创建AddContactDialog, 点击FloatingActionButton,跳出添加联系的弹窗。
在这里插入图片描述

@Composable
fun AddContactDialog(
    state: ContactState,
    onEvent: (ContactEvent) -> Unit,
    modifier: Modifier = Modifier
) {
    AlertDialog(
        modifier = modifier,
        onDismissRequest = {
            onEvent(ContactEvent.HideDialog)
        },
        title = { Text(text = "Add contact") },
        text = {
            Column(
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                TextField(
                    value = state.firstName,
                    onValueChange = {
                        onEvent(ContactEvent.SetFirstName(it))
                    },
                    placeholder = {
                        Text(text = "First name")
                    }
                )
                TextField(
                    value = state.lastName,
                    onValueChange = {
                        onEvent(ContactEvent.SetLastName(it))
                    },
                    placeholder = {
                        Text(text = "Last name")
                    }
                )
                TextField(
                    value = state.phoneNumber,
                    onValueChange = {
                        onEvent(ContactEvent.SetPhoneNumber(it))
                    },
                    placeholder = {
                        Text(text = "Phone number")
                    }
                )
            }
        },
        buttons = {
            Box(
                modifier = Modifier.fillMaxWidth(),
                contentAlignment = Alignment.CenterEnd
            ) {
                Button(onClick = {
                    onEvent(ContactEvent.SaveContact)
                }) {
                    Text(text = "Save")
                }
            }
        }
    )
}

ContactScreen 显示联系人列表的布局
在这里插入图片描述

@Composable
fun ContactScreen(
    state: ContactState,
    onEvent: (ContactEvent) -> Unit
) {
    Scaffold(
        floatingActionButton = {
            FloatingActionButton(onClick = {
                onEvent(ContactEvent.ShowDialog)
            }) {
                Icon(
                    imageVector = Icons.Default.Add,
                    contentDescription = "Add contact"
                )
            }
        },
    ) { _ ->
        if(state.isAddingContact) {
            AddContactDialog(state = state, onEvent = onEvent)
        }

        LazyColumn(
            contentPadding = PaddingValues(16.dp),
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            item {
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .horizontalScroll(rememberScrollState()),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    SortType.values().forEach { sortType ->
                        Row(
                            modifier = Modifier//点击文字部分也能够实现排序
                                .clickable {
                                    onEvent(ContactEvent.SortContacts(sortType))
                                },
                            verticalAlignment = CenterVertically
                        ) {
                            RadioButton(
                                selected = state.sortType == sortType,
                                onClick = {
                                    onEvent(ContactEvent.SortContacts(sortType))
                                }
                            )
                            Text(text = sortType.name)
                        }
                    }
                }
            }
            items(state.contacts) { contact -> 
                Row(
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Column(
                        modifier = Modifier.weight(1f)
                    ) {
                        Text(
                            text = "${contact.firstName} ${contact.lastName}",
                            fontSize = 20.sp
                        )
                        Text(text = contact.phoneNumber, fontSize = 12.sp)
                    }
                    IconButton(onClick = {
                        onEvent(ContactEvent.DeleteContact(contact))
                    }) {
                        Icon(
                            imageVector = Icons.Default.Delete,
                            contentDescription = "Delete contact"
                        )
                    }
                }
            }
        }
    }
}

MainActivity中延迟加载数据表db,ContactScreen( )需要传入viewModel的onEvent函数,而ContactViewModel类需要dao的依赖。建立 ViewModelProvider.Factory ,如果在构造函数中添加参数,则必须创建自己的 ViewModelProvider.Factory 实现来创建 ViewModel 实例。
ViewModelProvider.Factory 被用来创建 ContactViewModel 的实例。原因如下:
依赖注入:ContactViewModel 需要一个 ContactDatabase.Dao 实例来与其数据库进行交互。通过使用工厂模式,你可以在创建 ContactViewModel 实例时注入所需的依赖。
类型安全:通过工厂模式,你可以确保返回的 ViewModel 实例是正确的类型(在这里是 ContactViewModel)。如果没有工厂模式,你可能需要执行一个类型转换(这可能导致运行时错误)。
延迟初始化:由于 db 是通过 lazy 属性提供的,这意味着它只会在第一次被访问时初始化。通过使用工厂模式,你可以确保 ContactViewModel 是在数据库已经初始化之后才被创建的。

class MainActivity : ComponentActivity() {

    private val db by lazy {
        Room.databaseBuilder(
            applicationContext,
            ContactDatabase::class.java,
            "contacts.db"
        ).build()
    }
    private val viewModel by viewModels<ContactViewModel>(
        factoryProducer = {
            object : ViewModelProvider.Factory {
                override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                    return ContactViewModel(db.dao) as T
                }
            }
        }
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            RoomGuideAndroidTheme {
                val state by viewModel.state.collectAsState()
                ContactScreen(state = state, onEvent = viewModel::onEvent)
            }
        }
    }
}

参见以下文章:
ViewModelProvider.Factory 的作用和使用方式
Kotlin Flows 系列教程
郭神的Flow三部曲:
Kotlin Flow响应式编程,基础知识入门
Kotlin Flow响应式编程,操作符函数进阶
Kotlin Flow响应式编程,StateFlow和SharedFlow

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1382607.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构】树和二叉树堆(基本概念介绍)

&#x1f308;个人主页&#xff1a;秦jh__https://blog.csdn.net/qinjh_?spm1010.2135.3001.5343&#x1f525; 系列专栏&#xff1a;《数据结构》https://blog.csdn.net/qinjh_/category_12536791.html?spm1001.2014.3001.5482 ​​ 目录 前言 树的概念 树的常见名词 树与…

【OpenGauss源码学习 —— 执行器(execMain)】

执行器&#xff08;execMain&#xff09; 概述文件内容作用执行的操作主要函数概述 部分函数详细分析ExecutorStart 函数standard_ExecutorStart 函数 ExecutorRun 函数standard_ExecutorRun 函数 ExecutorFinish 函数standard_ExecutorFinish 函数 ExecutorEnd 函数standard_E…

[软件工具]通用OCR识别文字识别中文识别服务程序可局域网访问

【软件界面】 【算法介绍】 采用业界最先进算法之一paddlocr&#xff0c;PaddleOCR&#xff0c;全称PaddlePaddle OCR&#xff0c;是一种基于深度学习的光学字符识别&#xff08;OCR&#xff09;技术。相较于传统的OCR技术&#xff0c;PaddleOCR具有许多优点。 首先&#xff0…

windows下cmake Could NOT find Eigen3 (missing: Eigen3_DIR)解决办法-配置eigen3

在windows下用cmake构建项目&#xff0c;提示Could NOT find Eigen3 (missing: Eigen3_DIR) windows自行编译配置方法:参考这篇博客&#xff0c;我的windows没安装&#xff0c;使用的是VS2019&#xff0c;没有差别。 Windows安装配置eigen3 重新编译&#xff0c;这时候可能会…

NUS CS1101S:SICP JavaScript 描述:前言、序言和致谢

前言 原文&#xff1a;Foreword 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 我有幸在我还是学生的时候见到了了不起的 Alan Perlis&#xff0c;并和他交谈了几次。他和我共同深爱和尊重两种非常不同的编程语言&#xff1a;Lisp 和 APL。跟随他的脚步是一项艰巨的任…

插件化简单介绍

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、商业变现、人工智能等&#xff0c;希望大家多多支持。 未经允许不得转载 目录 一、导读二、概览三、常见的插件化方案…

深入理解 Spark(三)SparkTask 执行与 shuffle 详解

SparkTask 的分发部署与启动流程分析 Spark Action 算子触发 job 提交 Spark 当中 Stage 切分源码详解 Task 的提交与执行 SparkShuffle 机制详解 MapReduceShuffle 全流程深度剖析 MapReduce 全流程执行过程中参与工作的组件以及他们的执行先后顺序&#xff1a;InputFormat …

山西电力市场日前价格预测【2024-01-15】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2024-01-15&#xff09;山西电力市场全天平均日前电价为399.10元/MWh。其中&#xff0c;最高日前电价为583.33元/MWh&#xff0c;预计出现在18:15。最低日前电价为275.09元/MWh&#xff0c;预计…

分享从零开始学习网络设备配置--任务4.4 使用动态路由OSPFv3实现网络连通

任务描述 由于RIPng不适用于复杂的网络&#xff0c;考虑到公司的未来发展&#xff0c;需要不断扩大网络规模。某公司在企业网络升级时&#xff0c;选择 OSPFv3路由协议实现网络连通&#xff0c;降低网络拓扑变化引发的人工维护工作量并加快网络收敛的速度。 公司内部的所有设…

leetcode 2114. 句子中的最多单词数

题目&#xff1a; 一个 句子 由一些 单词 以及它们之间的单个空格组成&#xff0c;句子的开头和结尾不会有多余空格。 给你一个字符串数组 sentences &#xff0c;其中 sentences[i] 表示单个 句子 。 请你返回单个句子里 单词的最多数目 。 解题方法&#xff1a; 1.遍历列表…

vivado IP使用

使用IP源 注意&#xff1a;有关IP的更多信息&#xff0c;包括添加、打包、模拟和升级IP&#xff0c;请参阅VivadoDesign Suite用户指南&#xff1a;使用IP&#xff08;UG896&#xff09;进行设计。在Vivado IDE中&#xff0c;您可以在RTL项目中添加和管理以下类型的IP核心&…

一分钟找到所有的中文核心期刊

1.进入中国知网找到出版物检索 2.在出版来源导航这里选择期刊导航 3.右边拉到底选择核心期刊导航 4.选择自己专业的期刊即可

SpringMVC 学习博客记录

文章目录 Servlet请求转发和请求包含RequestDispatcher HandlerInterceptor组件实际运用场景 HandlerMapping&RequestMappingInfo(HandlerMapping)HandlerExecutionChainHandlerAdapter源码学习知识点博客记录 Servlet请求转发和请求包含 RequestDispatcher Request#getR…

微服务技术要点

一、服务注册到nacos 1.下载nacos&#xff0c;修改nacos启动模式为单机模式&#xff0c;另外需要在环境变量配置JAVA_HOME,否则启动不起来。 2.启动类加注解EnableDiscoveryClient 3.application.yml配置nacos地址 spring:cloud:nacos:discovery:server-addr: 127.0.0.1:884…

JDK-JVM

JVM JDKJDK内部体系结构&#xff1a;JVM 与 跨平台JVM在程序运行过程中的运行细节&#xff0c;内存分配 和 流转模型。JVM结构体系1. 虚拟机栈2. 线程栈2.1. 栈帧2.2. 数据结构栈 与 线程栈 的关系&#xff1a;2.3.栈帧的内部结构&#xff1a;2.4 方法中的数据 在栈帧中的流转过…

MyBatis第三课

目录 回顾 #和$区别 #&#xff08;预编译SQL&#xff09;和$&#xff08;即时SQL&#xff0c;它是进行的字符串拼接&#xff09;的区别&#xff0c;其中之一就是预编译SQL和即时SQL的区别 原因&#xff1a; 回顾 两者的共同点 MaBits可以看作是Java程序和Mysql的沟通桥梁&…

网页设计与网站建设作业html+css+js,一个简易的游戏官网网页

一个简易的游戏网页 浏览器查看 目录结构 部分代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport&…

【c++】利用嵌套map创建多层树结构

通常树的深度都大于1&#xff0c;即树有多层&#xff0c;而树结构又可以用c的map容器来实现&#xff0c;所以&#xff0c;本文给出了一种多层树结构的实现思路&#xff0c;同时也给出了相应的c代码。 整体思路概述 首先定义一个节点类Node类&#xff0c;要包括children&#x…

EI论文复现:考虑多能互补的综合能源系统/虚拟电厂/微电网优化运行程序代码!

本程序参考EI论文《基于多能互补的热电联供型微网优化运行》&#xff0c;文章通过储能设备解耦热电联系&#xff0c;建立基于多能互补的综合能源系统/虚拟电厂/微电网优化运行模型。模型包含系统供给侧的多能互补协调与需求侧的综合能源响应两个方面&#xff0c;使供给侧通过能…

基于springboot时间管理系统源码和论文

在Internet高速发展的今天&#xff0c;我们生活的各个领域都涉及到计算机的应用&#xff0c;其中包括时间管理系统的网络应用&#xff0c;在外国时间管理系统已经是很普遍的方式&#xff0c;不过国内的管理系统可能还处于起步阶段。时间管理系统具有时间管理功能的选择。时间管…