学习使用响应式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