Jetpack Flow 、Room 初学者学习记录

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

  • 19
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于 Room 数据库的升级,您需要遵循以下步骤: 1. 在新版本的数据库中定义新表结构或对旧表结构进行更改,例如添加、删除或修改表的列。 2. 在您的 `AppDatabase` 类中增加数据库版本号,可以在类上使用 `@Database` 注解指定版本号,例如: ```kotlin @Database(entities = [User::class], version = 2) abstract class AppDatabase : RoomDatabase() { //... } ``` 3. 创建一个实现 `Migration` 接口的类,该类将包含从旧版本升级到新版本所需的所有更改。例如: ```kotlin val migration_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0") } } ``` 该示例代码表示,从版本 1 升级到版本 2,需要在 `users` 表中添加一个名为 `age` 的整数类型的列。 4. 在 `AppDatabase` 类中,使用 `addMigrations()` 方法将 `Migration` 对象添加到数据库中,例如: ```kotlin @Database(entities = [User::class], version = 2) abstract class AppDatabase : RoomDatabase() { //... companion object { val migration_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0") } } } //... init { if (BuildConfig.DEBUG) { // 在调试模式下,如果发现数据结构变化,将会清空数据 fallbackToDestructiveMigration() } else { // 在正式发布模式下,如果发现数据结构变化,将会执行升级脚本 addMigrations(migration_1_2) } } } ``` 在上述示例代码中,我们将 `migration_1_2` 对象添加到 `AppDatabase` 类的伴生对象中,并在 `init` 块中进行了初始化。我们还使用了 `fallbackToDestructiveMigration()` 方法,如果在调试模式下发现数据结构变化,将会清空数据。在正式发布模式下,我们使用了 `addMigrations()` 方法,将 `migration_1_2` 对象添加到数据库中,以执行升级脚本。 这样,在您的应用程序使用新版本的数据库时,将自动执行升级脚本,以将旧数据结构转换为新数据结构。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值