为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应用开发的概念和技术。

相关推荐
JavaNoober21 分钟前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿1 小时前
关于ObjectAnimator
android
zhangphil2 小时前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我3 小时前
从头写一个自己的app
android·前端·flutter
lichong9514 小时前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端
用户69371750013844 小时前
14.Kotlin 类:类的形态(一):抽象类 (Abstract Class)
android·后端·kotlin
火柴就是我5 小时前
NekoBoxForAndroid 编译libcore.aar
android
Kaede66 小时前
MySQL中如何使用命令行修改root密码
android·mysql·adb
明君879977 小时前
Flutter 图纸标注功能的实现:踩坑与架构设计
android·ios
成都大菠萝7 小时前
Android Auto开发(3)-Audio Integration
android