一. Compose中的状态简介
“组合”是对 Compose 在执行可组合项时所构建界面的描述。Compose 应用调用可组合函数,以将数据转换为界面。如果发生状态更改,Compose 会使用新状态重新执行受影响的可组合函数,从而创建更新后的界面。这一过程称为“重组”。Compose 会为您安排重组。
当 Compose 首次运行可组合函数时,在初始组合期间,它会跟踪您为了描述组合中的界面而调用的可组合函数。重组是指 Compose 重新执行可能因数据更改而更改的可组合项,然后更新组合以反映所有更改
组合只能通过初始组合生成且只能通过重组进行更新。重组是修改组合的唯一方式。为此,Compose 需要知道要跟踪的状态,以便在收到更新时能够安排重组
在Compose中,您可以使用State和MutableState类型让应用中的状态可被Compose 观察或跟踪。State类型不可变,因此您只能读取其中的值,而MutableState类型是可变的。您可以使用mutableStateOf()函数来创建可观察的MutableState。它接受初始值作为封装在 State 对象中的形参,这样便可使其 value 变为可观察。
mutableStateOf()函数返回的值:
- 会保持状态
- 可变,因此值可以修改
- 可观察,因此 Compose 会观察值的所有更改并触发重组以更新界面。
二. 组合的状态
2.1 MutableState(可变状态)
下面我们通过一个TextFiled内容改变来探索下状态变化
示例代码:
@ComposablefunEditNumberField(modifier:Modifier=Modifier){varamountInput="0"TextField(value=amountInput,onValueChange={},modifier=modifier)}- 使用
MutableState<String>类型(而非硬编码的String变量),以便 Compose 知道要跟踪amountInput状态,然后传入 “0” 字符串,该字符串是amountInput状态变量的初始默认值
varamountInput:MutableState<String>=mutableStateOf("0")// 也可以使用类型推断编写 amountInput 初始化,如下所示:varamountInput=mutableStateOf("0")mutableStateOf() 函数接受初始值 “0” 作为实参,这样便可使其 amountInput 变为可观察
- 在
TextFiled组合中使用amountInput.value属性
TextField(value=amountInput.value,onValueChange={},modifier=modifier)Compose 会跟踪每个读取状态value属性的可组合项,并在其value更改时触发重组。
当文本框的输入更改时,系统会触发onValueChange回调。在 lambda 表达式中,it变量包含新值。
@ComposablefunEditNumberField(modifier:Modifier=Modifier){varamountInput=mutableStateOf("0")TextField(value=amountInput.value,onValueChange={amountInput.value=it},modifier=modifier)}当TextField通过onValueChange回调函数通知您文本发生更改时,您将更新TextField的状态(即amountInput变量)。
当你运行程序时发现, 不管你怎么输入,输入框的内容总是保持0不变。
原因如下:
用户在文本框中输入文本后,系统会调用onValueChange回调,并使用新值更新amountInput变量。Compose 跟踪amountInput状态,因此当其值更改时,系统会安排重组并再次执行EditNumberField()可组合函数。在该可组合函数中,amountInput变量会重置为初始 0 值。因此,文本框会显示 0 值。
2.2. remember(记住状态)
可组合方法可能会因重组而被系统多次调用。如果不保存,可组合项就会在重组期间重置状态。
可组合函数可以使用remember跨重组存储对象。初始组合期间,remember函数计算的值会存储在组合中,而存储的值会在重组期间返回。remember和mutableStateOf函数通常在可组合函数中一起使用,以使状态及其更新正确反映在界面中
// 使用 remember 将对 mutableStateOf() 的调用括起来,以便使用 by remember Kotlin 属性委托来初始化 amountInput 变量。amountInput 属性的默认 getter 和 setter 函数分别委托给 remember 类的 getter 和 setter 函数。// by 是Kotlin的属性委托语法// var amountInput by remember { mutableStateOf("") } 效果是把mutableStateOf的 .value读写操作委托给amountInput变量// 这样你就可以直接使用amountInput代替amountInput.value// var amountInput = remember { mutableStateOf("") }// 修改值: amountInput.value = "测试"// 读取值: println(amountInput.value)varamountInputbyremember{mutableStateOf("")}更新后的代码如下:
@ComposablefunEditNumberField(modifier:Modifier=Modifier){// 初始化常量金额varamountInputbyremember{mutableStateOf("")}TextField(value=amountInput,onValueChange={amountInput=it},modifier=modifier)}注意:在初始组合期间,TextField 中的 value 会设置为初始值,该值为一个空字符串。
当用户在文本字段中输入文本时,系统会调用 onValueChange lambda 回调,执行 lambda,并将 amountInput.value 设置为在文本字段中输入的更新后的值。
amountInput 是 Compose 跟踪的可变状态,系统会安排重组。EditNumberField() 可组合函数会重组。由于您使用的是 remember { }, 因此相应更改在重组后会保持不变,这就是状态不会重新初始化为 “” 的原因。
文本字段的 value 已设置为记住的 amountInput 值。文本字段会重组(使用新值在屏幕上重新绘制)。
2.3. 状态提升
在可组合函数中,您可以定义一些变量,用于保存要在界面中显示的状态。例如,您在EditNumberField()可组合项中将amountInput变量定义为状态。
当您的应用变得越来越复杂并且其他可组合项需要访问 EditNumberField() 可组合项中的状态时,您需要考虑将 EditNumberField() 可组合函数中的状态提升或提取出来。
当您需要执行以下操作时,应该提升状态:
- 与多个可组合函数共享状态。
- 创建可在应用中重复使用的无状态可组合项
在您从可组合函数中提取状态后,生成的可组合函数称为无状态函数。也就是说,通过从可组合函数中提取状态,可以将其变为无状态。
无状态可组合项是指没有状态的可组合项,这意味着它不会保存、定义或修改新状态。相反,有状态可组合项是指具有可以随时间变化的状态的可组合项。
提升状态后的代码:
@ComposablefunTipTimeLayout(){// 初始化常量金额varamountInputbyremember{mutableStateOf("")}// 计算小费valamount=amountInput.toDoubleOrNull()?:0.0valtip=calculateTip(amount)Column(modifier=Modifier.statusBarsPadding().padding(horizontal=40.dp).safeDrawingPadding(),horizontalAlignment=Alignment.CenterHorizontally,verticalArrangement=Arrangement.Center){Text(text=stringResource(R.string.calculate_tip),modifier=Modifier.padding(bottom=16.dp,top=40.dp).align(alignment=Alignment.Start))// 文本输入框EditNumberField(value=amountInput,onValueChange={amountInput=it},modifier=Modifier.padding(bottom=32.dp).fillMaxWidth())Text(text=stringResource(R.string.tip_amount,tip),style=MaterialTheme.typography.displaySmall)Spacer(modifier=Modifier.height(150.dp))}}// 把该方法内的状态提升到上一级方法, 然后通过参数和回调方法来 更新显示状态@ComposablefunEditNumberField(value:String,onValueChange:(String)->Unit,modifier:Modifier=Modifier){TextField(label={Text(stringResource(R.string.bill_amount))},// 添加文本标签singleLine=true,// 将文本框从多行压缩成可水平滚动的单行keyboardOptions=KeyboardOptions(keyboardType=KeyboardType.Number),// 指定键盘类型value=value,onValueChange=onValueChange,modifier=modifier)}2.4. 代码示例最终代码
classMainActivity:ComponentActivity(){overridefunonCreate(savedInstanceState:Bundle?){enableEdgeToEdge()super.onCreate(savedInstanceState)setContent{TipTimeTheme{Surface(modifier=Modifier.fillMaxSize(),){TipTimeLayout()}}}}}@ComposablefunTipTimeLayout(){// 初始化常量金额varamountInputbyremember{mutableStateOf("")}vartipInputbyremember{mutableStateOf("")}varroundUpbyremember{mutableStateOf(false)}// 计算小费valamount=amountInput.toDoubleOrNull()?:0.0valtipPercent=tipInput.toDoubleOrNull()?:15.0valtip=calculateTip(amount,tipPercent,roundUp)Column(modifier=Modifier.statusBarsPadding().padding(horizontal=40.dp).safeDrawingPadding().verticalScroll(rememberScrollState()),//向修饰符添加 .verticalScroll(rememberScrollState()),使列能够垂直滚动。rememberScrollState() 会创建并自动记住滚动状态。horizontalAlignment=Alignment.CenterHorizontally,verticalArrangement=Arrangement.Center,){Text(text=stringResource(R.string.calculate_tip),modifier=Modifier.padding(bottom=16.dp,top=40.dp).align(alignment=Alignment.Start))// 文本输入框 (金额)EditNumberField(R.string.bill_amount,leadingIcon=R.drawable.money,value=amountInput,onValueChange={amountInput=it},keyboardOptions=KeyboardOptions(keyboardType=KeyboardType.Number,imeAction=ImeAction.Next),modifier=Modifier.padding(bottom=32.dp).fillMaxWidth())// 小费 文本输入框EditNumberField(R.string.how_was_the_service,leadingIcon=R.drawable.percent,value=tipInput,onValueChange={tipInput=it},keyboardOptions=KeyboardOptions(keyboardType=KeyboardType.Number,imeAction=ImeAction.Done),modifier=Modifier.padding(bottom=32.dp).fillMaxWidth())// 是否小费取整的UIRoundTheTipRow(roundUp,onCheckedChange={roundUp=it},Modifier.padding(bottom=32.dp))Text(text=stringResource(R.string.tip_amount,tip),style=MaterialTheme.typography.displaySmall)Spacer(modifier=Modifier.height(150.dp))}}@ComposablefunEditNumberField(@StringReslabel:Int,// 为该参数添加注解, 表示 label 形参应为字符串资源引用, @StringRes 注解是一种类型安全的字符串资源使用方法,用于表明要传递的整数是 values/strings.xml 文件中的字符串资源。@DrawableResleadingIcon:Int,value:String,onValueChange:(String)->Unit,keyboardOptions:KeyboardOptions,modifier:Modifier=Modifier){TextField(label={Text(stringResource(label))},// 添加文本标签leadingIcon={Icon(painter=painterResource(leadingIcon),null)},// 添加左侧的icon图标singleLine=true,// 将文本框从多行压缩成可水平滚动的单行keyboardOptions=keyboardOptions,// 指定键盘类型value=value,onValueChange=onValueChange,modifier=modifier)}@ComposablefunRoundTheTipRow(roundUp:Boolean,onCheckedChange:(Boolean)->Unit,modifier:Modifier=Modifier){Row(modifier=modifier.fillMaxWidth().height(48.dp),verticalAlignment=Alignment.CenterVertically,horizontalArrangement=Arrangement.SpaceBetween){Text(stringResource(R.string.round_up_tip))Switch(checked=roundUp,onCheckedChange=onCheckedChange,)}}/** * Calculates the tip based on the user input and format the tip amount * according to the local currency. * Example would be "$10.00". */privatefuncalculateTip(amount:Double,tipPercent:Double=15.0,roundUp:Boolean):String{vartip=tipPercent/100*amountif(roundUp){tip=kotlin.math.ceil(tip)// 向上取整函数}returnNumberFormat.getCurrencyInstance().format(tip)}