本章是一个应用上一章:设计原则中学到的概念的项目。
项目的目标包括以下实现:
• 创建一个应用程序,该应用程序使用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)中,我们可以这样表示这些属性:
var name by remember { mutableStateOf("")}
var phone by remember { mutableStateOf("")}
那么,“Pay order” 按钮的((enable/disable)属性的状态呢?
在这种情况下,这个状态是由其他两个状态:姓名和电话号码衍生出来的。因此,这个状态不需要进一步定义。
View代码可能像这样:
@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”按钮的事件。 这些事件的处理方式如下:
//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中所提供的依赖项,声明依赖项:
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的完整实现如下:
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的结构中,如下所示:
data class FormUiStates(val name:String="",var phone:String="")
val FormUiStates.successValidated:Boolean get() = name.length>1 && phone.length==11
在ViewModel中,我们用一个单一的状态替换状态,如下所示:
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中,消息被简化为如下所示的一个:
@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中添加了以下改变:
// 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应用开发的概念和技术。