Android---Jetpack Compose学习003

news2025/1/22 19:35:21

Compose 状态。本文将探索如何在使用 Jetpack Compose 时使用和考虑状态,为此,我们需要构建一个 TODO 应用,我们将构建一个有状态界面,其中会显示可修改的互动式 TODO 列表。

状态的定义。在科学技术中,指物质系统所处的状态。也指各自聚集态,如物质的固、液、气等状态。当系统的温度、压力、体积、物态、物质的量、各种能量等等一定时,我就就说系统处于一个状态(state)

生活中的状态。比如红绿灯,它的状态有红、黄、绿三种状态。人的表情有哭、笑、生气等状态。

应用中的状态。指可以变化的任何值,这是一个非常宽泛的定义,从 Room 数据库到类的变量,全部涵盖在内。

所有 Android 应用都会向用户显示状态。下面是 Android 应用中的一些状态示例:

\bullet 在无法建立网络连接时显示的信息提示控件;

\bullet 博文和相关评论;

\bullet 在用户点击按钮时播放的波纹动画;

\bullet 用户可以在图片上绘制的贴纸。

无状态组件

显示一个可编辑的 TODO 列表,但它没有任何自己的状态

1. 添加依赖

    implementation 'com.google.android.material:material:1.5.0-alpha01'
    implementation 'androidx.appcompat:appcompat:1.4.0-alpha03'
    implementation 'androidx.compose.material:material:1.0.0-rc01'
    implementation 'androidx.compose.material:material-icons-extended:1.0.0-rc01'
    implementation 'androidx.compose.runtime:runtime-livedata:1.0.0-rc01'
    implementation 'androidx.compose.runtime:runtime:1.0.0-rc01'

 2. 初始化一些字符串。res-->values-->string.xml

<resources>
    <string name="app_name">JetpackComposeState</string>
    <string name="cd_expand">Expand</string>
    <string name="cd_collapse">Collapse</string>
    <string name="cd_crop_square">Crop</string>
    <string name="cd_done">Done</string>
    <string name="cd_event">Event</string>
    <string name="cd_privacy">Privacy</string>
    <string name="cd_restore">Restore</string>
</resources>

3. Data.kt

import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CropSquare
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Event
import androidx.compose.material.icons.filled.PrivacyTip
import androidx.compose.material.icons.filled.RestoreFromTrash
import androidx.compose.ui.graphics.vector.ImageVector
import com.example.jetpackcomposestate.R
import java.util.*

// 数据类
data class TodoItem(
    val task: String,
    val icon: TodoIcon = TodoIcon.Default,
    val id: UUID = UUID.randomUUID()
)
// 枚举类
enum class TodoIcon(
    val imageVector: ImageVector,
    @StringRes val contentDescription: Int
) {
    // 使用了Material Design的图标
    Square(Icons.Default.CropSquare, R.string.cd_expand),
    Done(Icons.Default.Done, R.string.cd_done),
    Event(Icons.Default.Event, R.string.cd_event),
    Privacy(Icons.Default.PrivacyTip, R.string.cd_privacy),
    Trash(Icons.Default.RestoreFromTrash, R.string.cd_restore);

    companion object {
        val Default = Square
    }
}

4. TodoScreen.kt 展示我们上面的静态页面

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.example.jetpackcomposestate.todo.TodoItem

/**
 * @Author HL
 * @Date 2023/12/30 16:04
 * @Version 1.0
 */
// 展示我们的静态页面
@Composable
fun TodoScreen(
    // TODO TodoItem 是我们在 Data.kt 中定义的数据类,有 task, icon, id 三个属性
    items : List<TodoItem>
){
    // 一列多行的布局,上面是又给列表,下面是一个Button
    Column {
        // 列表
        LazyColumn(
            modifier = Modifier
                .weight(1f),
            contentPadding = PaddingValues(top = 8.dp) // LazyColumn 里面内容的填充
        ){
            // 通过传入的 items 数据填充列表
            items(items){
                TodoRow(
                    todo = it,
                    modifier = Modifier.fillParentMaxWidth() // 让每一个 item 填充父容器的最大宽度
                )
            }
        }

        // 按钮
        Button(
            onClick = {  },
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth() // 设置最大宽度
        ) {
            Text(text = "Add random item")
        }
    }
}

// 将 items 里的每一条数据转换为 一行
@Composable
fun TodoRow(
    todo : TodoItem,
    modifier: Modifier = Modifier
){
    // 每一个 item 被布局为 1 行,左边是文本,右边是 icon
    Row (
        modifier = modifier
            .padding(horizontal = 16.dp, vertical = 8.dp),
        horizontalArrangement = Arrangement.SpaceBetween // 设置水平布局,子元素水平均匀分布
    ){
        Text(text = todo.task)

        Icon(
            imageVector = todo.icon.imageVector, //矢量图
            contentDescription = stringResource(id = todo.icon.contentDescription)
        )
    }
}

5. TodoActivity.kt。在 com.example.jetpackcomposestate.todo.one 目录下新建 TodoActivity.kt,并将项目的启动页设置为  TodoActivity.kt

class TodoActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JetpackComposeStateTheme {
                TodoActivityScreen()
            }
        }
    }

    @Composable
    fun TodoActivityScreen(){
        // 静态界面要显示的数据
        val items = listOf(
            TodoItem("Learn compose", TodoIcon.Event),
            TodoItem("Take the codelab"),
            TodoItem("Apply state", TodoIcon.Done),
            TodoItem("Build dynamic UIS", TodoIcon.Square)
        )
        TodoScreen(items = items)
    }
}

非结构化状态

UI 更新循环。是什么导致状态更新的?在 Android 应用程序中,状态会根据事件进行更新。事件是从我们的应用程序外部生成的输入,例如用户点击按钮。

\bullet 事件--事件由用户或程序的其它部分生成;

\bullet 更新状态--事件处理程序更改 UI 使用的状态;

\bullet 显示状态--更新 UI 以显示新状态。

上面的这种 UI 更新循环就叫做非结构化状态。在我们开始 Compose(Compose 是一种 结构化状态) 之前,让我们探索 Android 视图系统中的事件和状态。

非结构化状态中,当我们添加更多事件和状态时,可能会出现几个问题:

\bullet 测试,由于 UI 的状态与 Views 的代码交织在一起,因此很难测试此代码。

\bullet 部分状态更新,当屏幕有更多事件时,很容易忘记更新部分状态以响应事件。因此,用户肯恶搞会看到不一致或不正确的 UI。

\bullet 部分 UI 更新,由于我们在每次状态更改后手动更新 UI,因此有时很容易忘记这一点。因此,用户可能会在其 UI 中看到随机更新的陈旧数据。

\bullet 代码复杂性,在这种模式下编码时很难提取一些逻辑。结果,代码有变得难以阅读和理解的趋势。

单向数据流

为了帮助解决非结构化状态的这些问题,我们引入了 ViewModel 和 LiveData。我们将状态从 Activity 移到了 ViewModel,在 ViewModel 中,状态由 LiveData 表示。LiveData 是一种可观察状态容器,这意味着它可让任何人观察状态的变化。然后,我们在界面中使用 observe 方法,以便在状态变化时更新界面。

示例:实现如下功能。当我们在输入框中输入内容,会同步显示在上面 Text 中。

1. HelloComposeStateActivityWithViewModel.kt

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.jetpackcomposestate.databinding.ActivityHelloComposeStateBinding

/**
 * @Author HL
 * @Date 2023/12/30 20:52
 * @Version 1.0
 */

class HelloViewModel : ViewModel(){
    // _name 为一个状态
    private val _name = MutableLiveData("")
    val name : LiveData<String> = _name

    //2. 更新状态,进行 onNameChanged 处理,然后设置状态 _name
    fun onNameChanged(newName : String){
        _name.value = newName
    }

}

class HelloComposeStateActivityWithViewModel : ComponentActivity() {

    // 创建一个 ViewModel
    private val helloViewModel by viewModels<HelloViewModel>()
    // viewBinding,
    private val binding by lazy {
        ActivityHelloComposeStateBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        //1. 事件,onNameChanged 当文本输入更改时由 UI 调用
        binding.textInput.doAfterTextChanged { text ->
            //TODO 将事件“向上”流动到 ViewModel,由 UI 调用
            helloViewModel.onNameChanged(text.toString())
        }
        //3. 显示状态,name 的观察者被调用,通知 UI 状态变化
        helloViewModel.name.observe(this){ name ->
            // TODO 状态“向下”流动到Activity
            binding.helloText.text = "Hello, $name"
        }
    }
}

我们可以看到此 ViewModel 是如何与事件和状态配合工作的:

\bullet 事件,onNameChanged 当文本输入更改时由 UI 调用。即事件向上流动

\bullet 更新状态,进行 onNameChanged 处理,然后设置状态 _name

\bullet 显示状态,name 的观察者被调用,通知 UI 状态变化。即状态向下流动

通过以上这种方式构建代码,我们可以将事件“向上”流动到 ViewModel。然后,为了响应事件,ViewModel 将进行一些处理,而且可能会更新状态。状态更新后,会“向下”流动到Activity

单向数据流是一种状态向下流动而事件向上流动的设计,它的优势有:

\bullet 可测试性,通过将状态与显示它的 UI 分离,可以更轻松地测试 ViewModel 和 Activity。

\bullet 状态封装,因为状态只能在一个地方(the ViewModel)更新,随着 UI 的增长,你不太可能引入部分状态更新错误。

\bullet UI 一致性,所有状态更新都通过使用可观察状态者立即反映在 UI 中。

Compose 的状态管理

将上面的单向数据流应用到我们最开始写的那个静态页面中。当我们点击“按钮”时,往列表里随机的添加列表项。

状态提升把状态放到 ViewModel 里面。如果可组合项是无状态的,那它如何才能显示可修改的列表?为实现此目的,我们会使用一种称为状态提升的技术。Compose 中的状态提升是一种将状态移至可组合项的调用方以使可组合项无状态的模式。无状态组件更容易测试,往往有更少的错误,并提供更多的重用机会。

示例:

1. DataGenerators.kt --> 随机产生一个条目作为数据源

// 随机产生一个条目的数据源
fun generateRandomTodoItem(): TodoItem {
    val message = listOf(
        "Learn compose",
        "Learn state",
        "Build dynamic UIs",
        "Learn Unidirectional Data Flow",
        "Integrate LiveData",
        "Integrate ViewModel",
        "Remember to savedState!",
        "Build stateless composables",
        "Use state from stateless composables"
    ).random()
    val icon = TodoIcon.values().random()
    return TodoItem(message, icon)
}

2. 修改 TodoScreen.kt 。主要增加了点击事件时,增加和删除 TodoItem 的方法。

// 展示数据页面
@Composable
fun TodoScreen(
    // TODO TodoItem 是我们在 Data.kt 中定义的数据类,有 task, icon, id 三个属性
    items : List<TodoItem>,
    onAddItem : (TodoItem) -> Unit, //传一个匿名函数
    onRemoveItem : (TodoItem) -> Unit // 移除 item
){
    // 一列多行的布局,上面是又给列表,下面是一个Button
    Column {
        // 列表
        LazyColumn(
            modifier = Modifier
                .weight(1f),
            contentPadding = PaddingValues(top = 8.dp) // LazyColumn 里面内容的填充
        ){
            // 通过传入的 items 数据填充列表
            items(items){
                TodoRow(
                    todo = it,
                    onItemClicked = { onRemoveItem(it) }, // 当点击已有的 item 时,删除它
                    modifier = Modifier.fillParentMaxWidth() // 让每一个 item 填充父容器的最大宽度
                )
            }
        }

        // 按钮
        Button(
            // 点击按钮,触发事件,使用 generateRandomTodoItem 类随机生成一个 item
            onClick = { onAddItem(generateRandomTodoItem()) },
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth() // 设置最大宽度
        ) {
            Text(text = "Add random item")
        }
    }
}

// 将 items 里的每一条数据转换为 一行
@Composable
fun TodoRow(
    todo : TodoItem,
    onItemClicked : (TodoItem) -> Unit,
    modifier: Modifier = Modifier
){
    // 每一个 item 被布局为 1 行,左边是文本,右边是 icon
    Row (
        modifier = modifier
            .clickable { onItemClicked(todo) } // 当列表中的某个列表被点击时
            .padding(horizontal = 16.dp, vertical = 8.dp),
        horizontalArrangement = Arrangement.SpaceBetween // 设置水平布局,子元素水平均匀分布
    ){
        Text(text = todo.task)

        Icon(
            imageVector = todo.icon.imageVector, //矢量图
            contentDescription = stringResource(id = todo.icon.contentDescription)
        )
    }
}

3. TodoViewModel.kt 处理增加和删除事件,修改状态。 

class TodoViewModel : ViewModel(){
    // _todoItems 状态
    private var _todoItems = MutableLiveData(listOf<TodoItem>())

    val todoItems : LiveData<List<TodoItem>> = _todoItems

    // 事件:增加 item
    fun addItem(item : TodoItem){
        _todoItems.value = _todoItems.value!! + listOf(item)
    }

    // 事件:删除 item
    fun removeItem(item : TodoItem){
        _todoItems.value = _todoItems.value!!.toMutableList().also {
            it.remove(item)
        }
    }
}

4. 修改 TodoActivity.kt --> 修改后的状态向下流动到 Activity

class TodoActivity : ComponentActivity() {

    private val todoViewModel by viewModels<TodoViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JetpackComposeStateTheme {
                TodoActivityScreen()
            }
        }
    }

    @Composable
    fun TodoActivityScreen(){
//        // 静态界面要显示的数据
//        val items = listOf(
//            TodoItem("Learn compose", TodoIcon.Event),
//            TodoItem("Take the codelab"),
//            TodoItem("Apply state", TodoIcon.Done),
//            TodoItem("Build dynamic UIS", TodoIcon.Square)
//        )
//        TodoScreen(items = items)
        // 动态数据界面展示
        val items : List<TodoItem> by todoViewModel.todoItems.observeAsState(listOf())

        TodoScreen(
            items = items,
            onAddItem = {
                todoViewModel.addItem(it)
            },
            onRemoveItem = {
                todoViewModel.removeItem(it)
            })

    }
}

事实证明,这些参数的组合使得调用方能够从此可组合项中提升状态。为了了解具体的工作原理,我们来探索此可组合项的界面更新循环。

\bullet 事件--当用户请求添加或删除项时,TodoScreen 会调用 onAddItem 或 onRemoveItem

\bullet 更新状态--TodoScreen 的调用方可以通过更新状态来响应这些事件

\bullet 显示状态--状态更新后,系统将使用新的 itmes 再次调用 TodoScreen,而且后者可以在界面上显示它们。

调用方负责确定保存此状态的位置和方式。不过,它可以合理地存储 items,例如,存储在内存中或从 Room 数据库中读取。TodoScreen 与状态的管理方式是完全解耦的。

当应用于可组合项时,这通常意味着向可组合项引入两个参数

\bullet value: T - 要显示的当前值

\bullet onValueChange: (T) - Unit - 请求更改值的事件,其中 T 是建议的新值。

我们希望使用此 ViewModel 来提升 TodoScreen 中的状态。完成操作后,会创建如下所示的单向数据流设计:

MutableState

上面的示例是通过按钮随机生成一个 item,下面我来通过一个输入框,生成自己想要的 item。

示例:

1. TodoComponents.kt

// 输入框,TODO 输入的 text 就是状态
@Composable
fun TodoInputText(
    text : String, //TODO 状态
    onTextChange : (String) -> Unit, //TODO 改变状态
    modifier: Modifier = Modifier
){
    // 一个输入框
    TextField(
        value = text,
        onValueChange = onTextChange,
        colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent),// 设置输入框的颜色
        maxLines = 1, // 最多一行
        modifier = modifier //TODO modifier 等于传进来的 modifier
    )
}

// 按钮
@Composable
fun TodoEditButton(
    onButtonClick : () -> Unit,
    text : String, // 按钮上显示的文字,例如“add”,这里传入一个 text,以便这个按钮可以重用
    modifier: Modifier = Modifier,
    enabled : Boolean = true // 是否可以点击
){
    TextButton(
        onClick = onButtonClick,
        shape = CircleShape, // 圆角
        colors = ButtonDefaults.buttonColors(), // 设置按钮的颜色
        modifier = modifier, //TODO modifier 等于传进来的 modifier
        enabled = enabled // 是否可用
    ) {
        Text(text = text)
    }
}



@Composable
fun TodoItemInput(onItemComplete : (TodoItem) -> Unit){
    // TODO 创建一个状态,通过 MutableState 来创建一个 state
    /**
     * 这个函数使用 remember 给自己添加内存,然后在内存中存储一个由 mutableStateOf 创建的 MutableState<String>,
     * 它是 Compose 的内置类型,提供了一个可观察的状态持有者。
     * val (value, setValue) = remember{ (mutableStateOf(default) }
     * TODO 对 value 的任何更改都将  自动重新组合   读取   此状态  的任何可组合函数,比如这里的 Button 就会读取 text 的状态
     */
    val (text, setText) = remember{ mutableStateOf("")}

    Column {
        Row (
            modifier = Modifier
                .padding(horizontal = 16.dp)//设置水平填充,即左右
                .padding(top = 16.dp) // 单独设置 顶部 的填充
        ){
            // 输入框
            TodoInputText(
                text = text,
                onTextChange = setText,
                modifier = Modifier
                    .weight(1f)
                    .padding(end = 8.dp)
            )
            // 按钮
            TodoEditButton(
                onButtonClick = {
                    onItemComplete(TodoItem(text))
                    setText("")
                },
                text = "Add",
                modifier = Modifier
                    .align(Alignment.CenterVertically), // 设置按钮在父容器里垂直居中
                enabled = text.isNotBlank()// 按钮是否可用,取决于 text(状态) 是否为空
            )
        }
    }
}

2. TodoActivity.kt 

class TodoActivity : ComponentActivity() {

    private val todoViewModel by viewModels<TodoViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JetpackComposeStateTheme {
                //TodoActivityScreen()
                TodoItemInput(){ item ->
                    Log.d("HL", item.task)
                }
            }
        }
    }

如上图所示,当我们在输入框里输入内容时,文本改变(change),就会调用 setText(), setText()就会去改变我们的 MutableState 对象的 value 值。当它的 value 值发生改变的时候,就会重组,可组合函数 TodoEditButton() 就判断了 MutableState 的 text的value 值,即代码中的 isNotBlank()。如何空,那么 Button 就不可点击。不为空,Button 就可用点击。

这个函数使用 remember 给自己添加内存,然后在内存中存储一个由 mutableStateOf 创建的 MutableState<String>,它是 Compose 的内置类型,提供了一个可观察的状态持有者。val (value, setValue) = remember{ (mutableStateOf(default) },对 value 的任何更改都将  自动重新组合   读取   此状态  的任何可组合函数,比如这里的 Button 就会读取 text 的状态。

通过以下 MutableState 三种方式声明一个可组合对象:

\bullet val state = remember{ mutableStateOf(default) }

\bullet val value by remember{ mutableStateOf(default) }

\bullet val (value, setValue) = remember{ mutableStateOf(default) }

在组合中创建 State<T>(或其他有状态对象)时,请务必对其执行 remember 操作,否则它会在每次重组时重新初始化

MutableState<T> 类似于 MutableLiveData<T>,但 MutableState<T>与 Compose 在运行时已经集成了。由于它是可观察的,它会在更新时通知 Compose。

上面的示例代码中,还并没有在输入内容时弹出下面一排的图标框。通过下面的代码来完成。

修改 TodoComponents.kt 代码:

// 输入框,TODO 输入的 text 就是状态
@Composable
fun TodoInputText(
    text : String, //TODO 状态
    onTextChange : (String) -> Unit, //TODO 改变状态
    modifier: Modifier = Modifier
){
    // 一个输入框
    TextField(
        value = text,
        onValueChange = onTextChange,
        colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent),// 设置输入框的颜色
        maxLines = 1, // 最多一行
        modifier = modifier //TODO modifier 等于传进来的 modifier
    )
}

// 按钮
@Composable
fun TodoEditButton(
    onButtonClick : () -> Unit,
    text : String, // 按钮上显示的文字,例如“add”,这里传入一个 text,以便这个按钮可以重用
    modifier: Modifier = Modifier,
    enabled : Boolean = true // 是否可以点击
){
    TextButton(
        onClick = onButtonClick,
        shape = CircleShape, // 圆角
        colors = ButtonDefaults.buttonColors(), // 设置按钮的颜色
        modifier = modifier, //TODO modifier 等于传进来的 modifier
        enabled = enabled // 是否可用
    ) {
        Text(text = text)
    }
}

// 输入框下面的一排图标。输入框有内容,弹出图标;没有内容,收起图标。收起/弹出都带动画效果
@Composable
fun AnimatedIconRow(
    // TODO 我们可以选择图标,这里就有状态改变了,icon为状态,iconChange为状态后的处理
    icon : TodoIcon,
    onIconChange : (TodoIcon) -> Unit,
    modifier: Modifier = Modifier,
    visible : Boolean = true, // 图标是否可见
){
    // 进入动画 fadeIn 表示淡入淡出
    val enter = remember { fadeIn(animationSpec = TweenSpec(300, easing = FastOutLinearInEasing)) }
    // 退出动画
    val exit = remember { fadeOut(animationSpec = TweenSpec(100, easing = FastOutLinearInEasing)) }
    Box (Modifier.defaultMinSize(minHeight = 16.dp)){
        AnimatedVisibility(
            visible = visible,
            // 应用动画
            enter = enter,
            exit = exit
        ) {
            // TODO 在这里把动画效果应用到一排图标上
            IconRow(
                icon = icon,
                onIconChange = onIconChange,
                modifier = modifier
            )
        }
    }
}
// TODO 以动画的方式展示一排图标
@Composable
fun IconRow(
    icon: TodoIcon,
    onIconChange: (TodoIcon) -> Unit,
    modifier: Modifier = Modifier
){
    Row (modifier) {
        // 遍历我们的 Icon
        for(todoIcon in TodoIcon.values()){
            // TODO 把图标封装成一个组件
            SelectableIconButton(
                icon = todoIcon.imageVector,
                iconContentDescription = todoIcon.contentDescription,
                onIconSelected = { onIconChange(todoIcon) }, // 图标发生了改变,当前选中了一个,用户又点击另外一个
                isSelected = ( todoIcon == icon )  //icon 为传进来的图标,TODO 即选中的图标
            )
        }
    }
}

// 点击选择图标时,有下划线且颜色改变
@Composable
fun SelectableIconButton(
    icon: ImageVector,
    iconContentDescription: Int,
    onIconSelected: () -> Unit,
    isSelected: Boolean,
    modifier: Modifier = Modifier
) {
    //TODO 图标选中和未选中颜色不一样
    val tint = if (isSelected) { // 选中时颜色
        MaterialTheme.colors.primary
    }else{
        //onSurface 是黑色,通过修改它的透明度,来变成灰色
        MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
    }
    // 用一个 TextButton() 来构建一个图标
    TextButton(
        onClick = { onIconSelected() },
        shape = CircleShape, // 圆角
        modifier =  modifier
    ) {
        Column {
            // 图标,放到 TextButton 里
            Icon(
                imageVector = icon,
                tint = tint, // 设置图标颜色
                contentDescription = stringResource(id = iconContentDescription)
            )
            // TODO 如果图标被选中,用一个 Box() 来构建选中时的下划线
            if (isSelected) {
                Box(
                    modifier = Modifier
                        .padding(top = 3.dp)
                        .width(icon.defaultWidth)
                        .height(1.dp)
                        .background(tint) // 设置下划线颜色
                ) 
            }else{ // 没有选中,就没有下划线,但留出一个 4dp 的空间
                Spacer(modifier = Modifier.height(4.dp))
            }
        }
    }


}

@Composable
fun TodoItemInput(onItemComplete : (TodoItem) -> Unit){
    // TODO 创建一个状态,通过 MutableState 来创建一个 state
    /**
     * 这个函数使用 remember 给自己添加内存,然后在内存中存储一个由 mutableStateOf 创建的 MutableState<String>,
     * 它是 Compose 的内置类型,提供了一个可观察的状态持有者。
     * val (value, setValue) = remember{ (mutableStateOf(default) }
     * TODO 对 value 的任何更改都将  自动重新组合   读取   此状态  的任何可组合函数,比如这里的 Button 就会读取 text 的状态
     */
    val (text, setText) = remember{ mutableStateOf("")}
    val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default) } //TODO 点击选择图标也是一个状态,
    // TODO 图标列是否可见,取决于文本是否有内容
    val iconsVisible = text.isNotBlank()
    Column {
        Row (
            modifier = Modifier
                .padding(horizontal = 16.dp)//设置水平填充,即左右
                .padding(top = 16.dp) // 单独设置 顶部 的填充
        ){
            // 输入框
            TodoInputText(
                text = text,
                onTextChange = setText,
                modifier = Modifier
                    .weight(1f)
                    .padding(end = 8.dp)
            )
            // 按钮
            TodoEditButton(
                onButtonClick = {
                    onItemComplete(TodoItem(text))
                    setText("")
                },
                text = "Add",
                modifier = Modifier
                    .align(Alignment.CenterVertically), // 设置按钮在父容器里垂直居中
                enabled = text.isNotBlank()// 按钮是否可用,取决于 text(状态) 是否为空
            )
        }
        // TODO 根据文本是否有内容,来展示图标
        if (iconsVisible) {
            AnimatedIconRow(
                icon = icon,
                onIconChange = setIcon,
                modifier = Modifier.padding(top = 8.dp)
            )
        }else{// 图标不可见时,给下面留出一片空间
            Spacer(modifier = Modifier.height(16.dp))
        }
    }
}

Compose的状态恢复

rememberSaveable 恢复状态

在重新创建 Activity 或进程后,我们可以使用 rememberSaveable 恢复界面状态。rememberSaveable 可以在重组后保持状态。此外,rememberSaveable 也可以在重新创建 activity 和进程后保持状态。

存储状态的方式

添加到 Bundle 的所有数据类型都会自动保存。如果要保存无法添加到 Bundle 的内容,有以下几种选择:

a. Parcelize

 最简单的解决方案是向对象添加 @Parcelize 注解,对象将变为可打包状态并且可以捆绑。

b. MapSaver

如果某种原因导致 @Parcelize 不合适,可以使用 mapSaver 定义自己的规则,规定如何将对象转换为系统可保存到 Bundle 的一组值。

c. ListSaver

为了避免需要为映射定义键,也可以使用 listSaver 并将其索引用作键。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1445981.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C深度解剖】前置++与后置++

简介&#xff1a;本系列博客为C深度解剖系列内容&#xff0c;以某个点为中心进行相关详细拓展 适宜人群&#xff1a;已大体了解C语法同学 作者留言&#xff1a;本博客相关内容如需转载请注明出处&#xff0c;本人学疏才浅&#xff0c;难免存在些许错误&#xff0c;望留言指正 作…

相机图像质量研究(12)常见问题总结:光学结构对成像的影响--炫光

系列文章目录 相机图像质量研究(1)Camera成像流程介绍 相机图像质量研究(2)ISP专用平台调优介绍 相机图像质量研究(3)图像质量测试介绍 相机图像质量研究(4)常见问题总结&#xff1a;光学结构对成像的影响--焦距 相机图像质量研究(5)常见问题总结&#xff1a;光学结构对成…

关于Navicat导入sql文件后er图乱序问题

关于Navicat导入sql文件后er图乱序问题 具体操作如下&#xff1a; 点击左下角刷新旁边的小三角

涤生大数据实战:基于Flink+ODPS历史累计计算项目分析与优化(上)

涤生大数据实战&#xff1a;基于FlinkODPS历史累计计算项目分析与优化&#xff08;一&#xff09; 1.前置知识 ODPS&#xff08;Open Data Platform and Service&#xff09;是阿里云自研的一体化大数据计算平台和数据仓库产品&#xff0c;在集团内部离线作为离线数据处理和存…

数据结构(3) 顺序表存储结构

顺序表存储结构 存储结构顺序结构定义基本操作的实现静态分配问题 动态分配代码功能 顺序表的特点: 顺序表小结顺序表的插入删除插入删除小结 顺序表的查找按位查找按值查找小结 各位小伙伴想要博客相关资料的话关注公众号&#xff1a;chuanyeTry即可领取相关资料&#xff01; …

Codeforces Round 924 E. Modular Sequence

E. Modular Sequence 题意 对于一个长度为 n n n 的数组 a a a&#xff0c;定义它是 g o o d good good 的当且仅当&#xff1a; a 1 x a_1 x a1​x a i a i − 1 y a_{i} a_{i - 1} y ai​ai−1​y 或 a i a i − 1 m o d y i ≥ 2 a_{i} a_{i - 1} mod \hspace{…

【Linux】内核中的链表

&#x1f525;博客主页&#xff1a;PannLZ &#x1f38b;系列专栏&#xff1a;《Linux系统之路》 &#x1f94a;不要让自己再留有遗憾&#xff0c;加油吧&#xff01; 文章目录 链表1.创建和初始化2.创建节点3.添加节点4.删除节点5.遍历 链表 内核开发者只实现了循环双链表&am…

nodejs学习计划--(十)会话控制及https补充

一、会话控制 1.介绍 所谓会话控制就是 对会话进行控制 HTTP 是一种无状态的协议&#xff0c;它没有办法区分多次的请求是否来自于同一个客户端&#xff0c; 无法区分用户 而产品中又大量存在的这样的需求&#xff0c;所以我们需要通过 会话控制 来解决该问题 常见的会话控制…

中科大计网学习记录笔记(十):P2P 应用

前言&#xff1a; 学习视频&#xff1a;中科大郑烇、杨坚全套《计算机网络&#xff08;自顶向下方法 第7版&#xff0c;James F.Kurose&#xff0c;Keith W.Ross&#xff09;》课程 该视频是B站非常著名的计网学习视频&#xff0c;但相信很多朋友和我一样在听完前面的部分发现信…

CSP-动态规划-最长公共子序列(LCS)

一、动态规划 动态规划&#xff08;Dynamic Programming&#xff0c;简称DP&#xff09;主要用于求解可以被分解为相似子问题的复杂问题&#xff0c;特别是在优化问题上表现出色&#xff0c;如最短路径、最大子数组和、编辑距离等。动态规划的核心思想是将原问题分解为较小的子…

【MySQL】MySQL函数学习和总结

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-Ny0xnYjfHqF7s3aS {font-family:"trebuchet ms",verdana,arial,sans-serif;font-siz…

C++ //练习 6.3 编写你自己的fact函数,上机检查是否正确。

C Primer&#xff08;第5版&#xff09; 练习 6.3 练习 6.3 编写你自己的fact函数&#xff0c;上机检查是否正确。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#xff09; 工具&#xff1a;vim 代码块 /********************************************************…

Linux系统中HTTP代理的常见问题及解决方案

亲爱的Linux用户们&#xff0c;是不是有时候觉得HTTP代理就像是一个魔法盒子&#xff0c;让你在数字世界中自由穿梭&#xff1f;但是&#xff0c;就像所有的魔法物品一样&#xff0c;它也会偶尔出点小状况。今天&#xff0c;我们就来一起探讨一下Linux系统中HTTP代理的常见问题…

购物|电商购物小程序|基于微信小程序的购物系统设计与实现(源码+数据库+文档)

电商购物小程序目录 目录 基于微信小程序的购物系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、用户前台功能实现 2、管理员后台功能实现 四、数据库设计 1、实体ER图 2、具体的表设计如下所示&#xff1a; 五、核心代码 六、论文参考 七、最新计算机毕设…

【原创 附源码】Flutter安卓及iOS海外登录--Tiktok登录最详细流程

最近接触了几个海外登录的平台&#xff0c;踩了很多坑&#xff0c;也总结了很多东西&#xff0c;决定记录下来给路过的兄弟坐个参考&#xff0c;也留着以后留着回顾。更新时间为2024年2月7日&#xff0c;后续集成方式可能会有变动&#xff0c;所以目前的集成流程仅供参考&#…

【数据结构】14 队列(带头结点的链式存储和顺序存储实现)

定义 队列是一个有序线性表&#xff0c;但是队列的插入、删除操作是分别在线性表的两个不同端点进行的。 设一个队列 Q ( a 1 , a 2 , . . . , a n ) Q (a_1, a_2,...,a_n) Q(a1​,a2​,...,an​)&#xff0c;那么 a 1 a_1 a1​被称为队头元素&#xff0c; a n a_n an​为队…

漫漫数学之旅017

文章目录 经典格言数学习题古今评注名人小传&#xff08;一&#xff09;亚当斯密&#xff08;二&#xff09;J理查德高特三世 经典格言 科学是热情与迷信之毒的最佳解毒剂。——亚当斯密&#xff08;Adam Smith&#xff09; 咳咳&#xff0c;各位看官&#xff0c;且听我用轻松…

leetcode:买卖股票最佳时机二

思路&#xff1a; 使用贪心算法&#xff1a;局部最优是将买卖过程中产生的正数进行相加&#xff0c;进而使得最后结果最大&#xff08;全局最优&#xff09;。 price [7,1,5,10,3,6,4] -6,4,5,-7,3,-2 正数相加就得到了最大 代码实现&#xff1a; 1.循环中下标从1开始 …

文件包含知识点详细总结

如果想看图片和观感更好的话,可以直接去我的github或者gitbook github:https://github.com/kakaandhanhan/cybersecurity_knowledge_book-gitbook.22kaka.fun gitbook:http://22kaka.fun description: 这里将通过参考文章和做题一起进行总结,并且文件包含漏洞,很多都利用了…

CVE-2022-25487 漏洞复现

漏洞描述&#xff1a;Atom CMS 2.0版本存在远程代码执行漏洞&#xff0c;该漏洞源于/admin/uploads.php 未能正确过滤构造代码段的特殊元素。攻击者可利用该漏洞导致任意代码执行。 其实这就是一个文件上传漏洞罢了。。。。 打开之后&#xff0c;/home路由是个空白 信息搜集&…