为Android构建现代应用—— 练习状态管理

介绍

本章是一个应用上一章:设计原则中学到的概念的项目。

项目的目标包括以下实现:

• 创建一个应用程序,该应用程序使用View作为真实来源。

• 修改应用程序,使其使用ViewModel作为真实来源。

• 将状态和事件进行分组,以简化View和ViewModel之间的消息传递。

例如,在这个项目中,我们将实现电子商务的一部分屏幕。

该电子商务将在下章:OrderNow,一个真实应用程序中设计和开发的一个应用程序示例。

该屏幕将是OrderScreen,其中包含用户请求的订单信息和用户或买家的其他联系详情。

我们将在项目中只实现屏幕的一部分以简化。目标是练习管理状态的不同方式。

后面会有一个完整的案例:

https://github.com/yaircarreno/building-modern-apps-for-android-code/tree/main/chapter_02

我们的目标是实现一个包含两个字段的表单:

• User name

• Phone number

另外,"PayOrder"按钮的启用或禁用将取决于"User name"和"Phone number"字段的正确验证。以下将展示不同的实现选项。

"Views"作为真实源

首先,我们需要识别哪些UI元素可能会发生变化,并在屏幕上代表不同的状态。在这个例子中,它们是:

• 为用户名输入的文本值。

• 为电话号码输入的文本值。

• 支付订单按钮的启用/禁用属性。

因此,在View(Composables)中,我们可以这样表示这些属性:

Kotlin 复制代码
var name  by remember { mutableStateOf("")}
var phone by remember { mutableStateOf("")}

那么,"Pay order" 按钮的((enable/disable)属性的状态呢?

在这种情况下,这个状态是由其他两个状态:姓名和电话号码衍生出来的。因此,这个状态不需要进一步定义。

View代码可能像这样:

Kotlin 复制代码
@Preview
@Composable
fun OrderScreen1(){
    var name by remember{ mutableStateOf("")}
    var phone by remember { mutableStateOf("")}

    ContactInformation3(name=name, onNameChange = {name=it},phone=phone, onPhoneChange = {phone=it})
}

@Composable
fun ContactInformation3(name:String,onNameChange:(String)->Unit,phone:String,onPhoneChange:(String) ->Unit) {
    Column(modifier= Modifier
        .fillMaxSize()
        .padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally,){

        TextField(
            label={ Text( "User name") },
            value = name,
            onValueChange = onNameChange
        )
        Spacer(modifier = Modifier.padding(5.dp))
        TextField(
            label = { Text(text = "Phone number")},
            value = phone,
            onValueChange=onPhoneChange
        )
        Spacer(Modifier.padding(5.dp))
        Button(onClick = { println("Order generated for $name and phone $phone") }, enabled = name.length>1 && phone.length==11){
            Text(text = "Pay Order")
        }


    }
}

那么事件呢?

在这个屏幕示例中,我们识别出的事件有:

• "User name"被修改的事件。

• "Phone number"被修改的事件。

• 选中(点击)"Pay Order"按钮的事件。 这些事件的处理方式如下:

Kotlin 复制代码
	//User name changed
	onNameChange = { name = it }

	...

	//Phone number changed
	onPhoneChange = { phone = it }

	//Pay order clicked
	Button(
	    onClick = {
	        println("Order generated for $name and phone $phone")
	    },
	...
	)

注意!

使用ViewModel,项目中必须有Google在ViewModel中所提供的依赖项,声明依赖项:

Kotlin 复制代码
dependencies {
	def lifecycle_version = "2.5.0-rc01"

	// ViewModel
	implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"

	// ViewModel utilities for Compose
	implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
}

View的完整实现如下:

Kotlin 复制代码
class OrderViewModel:ViewModel() {
    var name by  mutableStateOf("")
    var onNameChange :(String)->Unit ={ name=it}
    var phone by mutableStateOf("")
    var onPhoneChange:(String)->Unit ={phone=it}
    var payOrder:()->Unit={
        println("name=$name  phone=$phone")
    }
}



@Preview
@Composable
fun OrderScreen(viewModel2: OrderViewModel = viewModel()) {
    ContactInformation4(viewModel2.name,viewModel2.onNameChange,viewModel2.phone,viewModel2.onPhoneChange,viewModel2.payOrder)
}

@Composable
fun ContactInformation4(name: String, onNameChange: (String) -> Unit, phone:String, onPhoneChange: (String) -> Unit, payOrder:()->Unit) {
    Column(modifier = Modifier
        .fillMaxSize()
        .padding(8.dp),horizontalAlignment=Alignment.CenterHorizontally){
        TextField(label = { Text(text = "User name")}, value = name, onValueChange = onNameChange )
        Spacer(modifier = Modifier.padding(5.dp))
        TextField(label = { Text(text = "phone number")},value=phone, onValueChange = onPhoneChange)
        Spacer(modifier = Modifier.padding(5.dp))
        Button(onClick = payOrder, enabled = name.length>1&& phone.length==11) {
            Text(text = "Pay order")
        }

    }
}

在这第二种实现选项中,我们看到状态和事件都被委托给了ViewModel;因此,ViewModel成为了真实源。

随着真实源的改变,设计获得了在ViewModel中应用集中式业务或表示逻辑的灵活性。 到目前为止,我们有了一个合适且工作正常的实现。但是,通过定义组件UI的状态,我们可以看到它可以得到进一步的改善。

分组"States"

在上面的例子中,你可以看到字段是表单的一部分。按钮的状态甚至依赖于表单的字段。因此,将这些UI元素组合成一个包含它们的单一UI元素是有意义的。 由于例子中只有三个UI元素,分组它们的好处可能不那么明显;然而,让我们想象一个屏幕,它有许多其他区域,包含许多其他的UI元素。 首先,将状态组合在一个名为FormUiState的结构中,如下所示:

Kotlin 复制代码
data class FormUiStates(val name:String="",var phone:String="")

val FormUiStates.successValidated:Boolean get() = name.length>1 && phone.length==11

在ViewModel中,我们用一个单一的状态替换状态,如下所示:

Kotlin 复制代码
data class FormUiStates(val name:String="",var phone:String="")

val FormUiStates.successValidated:Boolean get() = name.length>1 && phone.length==11




class OrderViewModel5:ViewModel() {
    //UI's states
    var formUiState by mutableStateOf(FormUiStates())
    private set
    //UI's Events
    fun onNameChange():(String)->Unit={
        formUiState=formUiState.copy(name=it)
    }

    fun onPhoneChange():(String)->Unit={
        formUiState= formUiState.copy(phone = it)
    }

    fun payOrder():() ->Unit={
        println("Order generated for ${formUiState.name} and phone ${formUiState.phone}")
    }
    
}


//在View中,我们按照如下方式更新状态的使用:
@Preview
@Composable
fun OrderScreen(viewModel5:OrderViewModel5= viewModel()){
    ContactInformation5(
        name = viewModel5.formUiState.name,
        onNameChange = viewModel5.onNameChange(),
        phone = viewModel5.formUiState.phone,
        onPhoneChange = viewModel5.onPhoneChange(),
        payOrder = viewModel5.payOrder(),
        isValidPayOrder = viewModel5.formUiState.successValidated)
}

@Composable
fun ContactInformation5(name: String,onNameChange: (String) -> Unit,phone: String,onPhoneChange: (String) -> Unit,payOrder: () -> Unit,isValidPayOrder:Boolean) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally
    ){
        TextField(label = { Text(text = "user name")}, value =name , onValueChange =onNameChange )
        Spacer(modifier = Modifier.padding(5.dp))
        TextField(label = { Text(text = "phone number")}, value =phone   , onValueChange =onPhoneChange )
        Spacer(modifier = Modifier.padding(5.dp))
        Button(onClick = payOrder, enabled = isValidPayOrder) {
            Text(text = "Pay Order")
        }
    }
}

当一个屏幕上有许多UI元素时,将相关的UI元素分组变得更加重要。将UI元素分组到组件UI的状态中可以简化、组织和生成在实现中更清晰的代码。 同样的技巧可以应用于事件。如下所示,主要的区别在于表示类型。

分组"Eents"

为了进一步整理代码,我们现在将分组表单的相关事件。首先,我们要创建一个结构来按以下方式分组事件:

我们注意到,事件的分组与上一章中的屏幕UI状态部分解释的技术类似。记住,这是适用的,因为我们定义的不同类型的事件是相关的,但它们可以是互斥的和独立的。

在ViewModel中,消息被简化为如下所示的一个:

Kotlin 复制代码
@Preview
@Composable
fun OrderScreen7(viewModel7:OrderViewModel7= viewModel()){
    ContactInformation7(
        name = viewModel7.formUiState.name,
        onNameChange = {viewModel7.onFormEvent(FormUiEvent.onNameChange(it))},
        phone = viewModel7.formUiState.phone,
        onPhoneChange = {viewModel7.onFormEvent(FormUiEvent.onPhoneChange(it))},
        payOrder = {viewModel7.onFormEvent(FormUiEvent.payOrderClicked)},
        isValidPayOrder = viewModel7.formUiState.successValidated
    )
}

@Composable
fun ContactInformation7(
    name: String,
    onNameChange: (String) -> Unit,
    phone:String,
    onPhoneChange:(String)->Unit,
    payOrder:()->Unit,
    isValidPayOrder:Boolean) {
    Column(modifier = Modifier
        .fillMaxSize()
        .padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally){
        TextField(label = { Text(text = "User name")}, value =name , onValueChange =onNameChange )
        Spacer(modifier = Modifier.padding(5.dp))
        TextField(value = phone, label = { Text(text = "phone number")}, onValueChange =onPhoneChange )
        Spacer(modifier = Modifier.padding(5.dp))
        Button(onClick = payOrder,
            enabled = isValidPayOrder) {
            Text(text = "Play Order")
        }
    }

}




class OrderViewModel7 :ViewModel() {
    //UI's states
    var formUiState by mutableStateOf(FormUiSatate6())
    private set
    //UI's Events
    fun onFormEvent(formEvent:FormUiEvent){
        when(formEvent){
            is FormUiEvent.onNameChange->{
                formUiState=formUiState.copy(name = formEvent.name)
            }
            is FormUiEvent.onPhoneChange->{
                formUiState=formUiState.copy(phone = formEvent.phone)
            }
            is FormUiEvent.payOrderClicked ->{
                println("Sending form with parameters:${formUiState.name} and phone ${formUiState.phone}")
            }
        }
    }
    //Business's logic or maybe some UI's logic for update the state
    companion object{
        fun applyLogicToValidateInputs(name:String,phone:String):Boolean{
            return name.length>1 && phone.length ==11
        }
    }
}






data class FormUiSatate6(val name:String="",val phone:String="")
val FormUiSatate6.successValidated:Boolean get()=name.length>1 && phone.length==11





sealed class FormUiEvent{
    data class onNameChange (val name:String):FormUiEvent()
    data class onPhoneChange(val phone:String):FormUiEvent()
    object payOrderClicked: FormUiEvent()
}

注意:

有些可能已经注意到,我将字段验证逻辑包含在了FormUiState状态结构中。 由于逻辑通常比验证字符长度更复杂,最好将验证和验证任务委托给ViewModel。 因此,我们在ViewModel和FormUiState中添加了以下改变:

Kotlin 复制代码
	// Business's logic or maybe some UI's logic for update the state
	companion object {
	fun applyLogicToValidateInputs(name: String, phone: String): Boolean {
	return name.length > 1 && phone.length > 3
	}
	}





data class FormUiState(
	val name: String = "",
	val phone: String = ""
	)

val FormUiState.successValidated: Boolean get() =OrderViewModel.applyLogicToValidateInputs(name, phone)

现在所有的逻辑都在ViewModel端了。

总结

在这个练习中,我们回顾了使用View或ViewModel作为真实源来管理状态和事件的方法。 此外,我们使用了一些技术来更好地组织状态和事件的结构,以便有一个更好组织和易于跟踪的实现。 在下一章中,我们将看到"立即下单"应用程序的总结,这是一个电子商务应用,我们将实现它,以解释现代Android应用开发的概念和技术。

相关推荐
HerayChen3 分钟前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
顾北川_野4 分钟前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
hairenjing11236 分钟前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机
小黄人软件32 分钟前
android浏览器源码 可输入地址或关键词搜索 android studio 2024 可开发可改地址
android·ide·android studio
dj15402252031 小时前
group_concat配置影响程序出bug
android·bug
周全全1 小时前
MySQL报错解决:The user specified as a definer (‘root‘@‘%‘) does not exist
android·数据库·mysql
- 羊羊不超越 -2 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
wk灬丨3 小时前
Android Kotlin Flow 冷流 热流
android·kotlin·flow
千雅爸爸3 小时前
Android MVVM demo(使用DataBinding,LiveData,Fresco,RecyclerView,Room,ViewModel 完成)
android