Android---Jetpack Compose学习003

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 并将其索引用作键。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【XR806开发板试用】轻松连上华为云实现物联网

本文为极术社区XR806试用活动文章。 一.开始 偶然的机会在网上看到了鸿蒙开发板的试用,作为一个"老鸿蒙"岂能放弃这个机会,报名之后不出意料地得到了使用名额,在此感谢极术社区. 收到开发板之后其实还有点失望了,就那么一个小小的核心板,其他啥也没有,连一根数据线…

图灵日记--MapSet字符串常量池反射枚举Lambda表达式泛型

目录 搜索树概念实现性能分析和 java 类集的关系 搜索概念及场景模型 Map的使用Map常用方法 Set的说明常见方法说明 哈希表冲突-避免-负载因子调节冲突-解决-闭散列冲突-解决-开散列/哈希桶冲突严重时的解决办法 实现和 java 类集的关系 字符串常量池String对象创建intern方法 …

MongoDB 与 mongo-express docker 安装

MongoDB 和 mongo-express 与 MySQL 不同&#xff0c;MongoDB 为 NoSQL 数据库&#xff0c;MongoDB 中没有 table &#xff0c;schema 概念&#xff0c;取而代之的 collection&#xff0c;其中 collection 存储的为 BSON 格式&#xff0c;是一种类似于 JSON 的用于存储 k-v 键…

每日五道java面试题之java基础篇(五)

第一题. final、finally、finalize 的区别&#xff1f; final ⽤于修饰变量、⽅法和类&#xff1a;final 修饰的类不可被继承&#xff1b;修饰的⽅法不可被重写&#xff1b;修饰的变量不可变。finally 作为异常处理的⼀部分&#xff0c;它只能在 try/catch 语句中&#xff0c;…

PyTorch深度学习实战(26)——多对象实例分割

PyTorch深度学习实战&#xff08;26&#xff09;——多对象实例分割 0. 前言1. 获取并准备数据2. 使用 Detectron2 训练实例分割模型3. 对新图像进行推断小结系列链接 0. 前言 我们已经学习了多种图像分割算法&#xff0c;在本节中&#xff0c;我们将学习如何使用 Detectron2 …

Netty Review - NioEventLoopGroup源码解析

文章目录 概述类继承关系源码分析小结 概述 EventLoopGroup bossGroup new NioEventLoopGroup(1); EventLoopGroup workerGroup new NioEventLoopGroup();这段代码是在使用Netty框架时常见的用法&#xff0c;用于创建两个不同的EventLoopGroup实例&#xff0c;一个用于处理连…

【计算几何】确定两条连续线段向左转还是向右转

确定两条连续线段向左转还是向右转 目录 一、说明二、算法2.1 两点的叉积2.2 两个段的叉积 三、旋转方向判别3.1 左转3.2 右转3.3 共线判别 一、说明 如果是作图&#xff0c;或者是判别小车轨迹。为了直观地了解&#xff0c;从当前点到下一个点过程中&#xff0c;什么是左转、…

树莓派4B(Raspberry Pi 4B)使用docker搭建阿里巴巴sentinel服务

树莓派4B&#xff08;Raspberry Pi 4B&#xff09;使用docker搭建阿里巴巴sentinel服务 由于国内访问不了docker hub&#xff0c;而国内镜像仓库又没有适配树莓派ARM架构的sentinel镜像&#xff0c;所以我们只能退而求其次——自己动手构建镜像。本文基于Ubuntu&#xff0c;Jav…

springboot169基于vue的工厂车间管理系统的设计

基于VUE的工厂车间管理系统设计与实现 摘 要 社会发展日新月异&#xff0c;用计算机应用实现数据管理功能已经算是很完善的了&#xff0c;但是随着移动互联网的到来&#xff0c;处理信息不再受制于地理位置的限制&#xff0c;处理信息及时高效&#xff0c;备受人们的喜爱。本…

书生谱语-大语言模型测试demo

课程内容简介 1.作业 demo1 demo2 demo3 demo4

Makefile编译原理 make 中的路径搜索_1

一.make中的路径搜索 问题&#xff1a;在实际的工程项目中&#xff0c;所有的源文件和头文件都放在同一个文件夹中吗&#xff1f; 实验1 &#xff1a; VPATH 引子 mhrubuntu:~/work/makefile1/17$ ll total 28 drwxrwxr-x 4 mhr mhr 4096 Apr 22 00:46 ./ drwxrwxr-x 7 mhr m…

《UE5_C++多人TPS完整教程》学习笔记10 ——《P11 设置加入游戏会话(Setup for Joining Sessions)》

本文为B站系列教学视频 《UE5_C多人TPS完整教程》 —— 《P11 设置加入游戏会话&#xff08;Setup for Joining Sessions&#xff09;》 的学习笔记&#xff0c;该系列教学视频为 Udemy 课程 《Unreal Engine 5 C Multiplayer Shooter》 的中文字幕翻译版&#xff0c;UP主&…

Python远程控制工具的使用

本节我们对所编写的远程控制工具的功能进行测试。首先开启主控端程序&#xff0c; 如下所示&#xff1a; 接下来打开被控端程序。当被控端打开时&#xff0c;主控端会收到被控端的连接请 求。 开启被控端程序&#xff1a; 主控端接收到连接请求并显示被控端主机的信息&#xff…

MySQL-----DCL基础操作

▶ DCL简介 DCL英文全称是Data ControlLanguage(数据控制语言)&#xff0c;用来管理数据库用户、控制数据库的访问权限。 DCL--管理用户 ▶ 查询用户 use mysql; select * from user; ▶ 创建用户 ▶ 语法 create user 用户名主机名 identified by 密码 设置为在任意主机上访问…

Z-Stack一直卡在HAL_BOARD_INIT();

原因是Debugger没有配置好&#xff0c;因为默认是Simulator&#xff0c;不是TI的驱动&#xff0c;所以仿真出现一直卡在 HAL_BOARD_INIT(); 的情况&#xff0c;解决方法就是将Simulator改为Texas Instruments 改成下面的样子

MySQL篇----第二十篇

系列文章目录 文章目录 系列文章目录前言一、NULL 是什么意思二、主键、外键和索引的区别?三、你可以用什么来确保表格里的字段只接受特定范围里的值?四、说说对 SQL 语句优化有哪些方法?(选择几条)前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍…

Educational Codeforces Round 135 (Rated for Div. 2)C. Digital Logarithm(思维)

文章目录 题目链接题意题解代码 题目链接 C. Digital Logarithm 题意 给两个长度位 n n n的数组 a a a、 b b b&#xff0c;一个操作 f f f 定义操作 f f f为&#xff0c; a [ i ] f ( a [ i ] ) a [ i ] a[i]f(a[i])a[i] a[i]f(a[i])a[i]的位数 求最少多少次操作可以使 …

单片机学习笔记---串口向电脑发送数据电脑通过串口控制LED

目录 串口向电脑发送数据 每隔一秒串口就发送一个递增的数给电脑 电脑通过串口控制LED 波特率的具体计算 HEX模式和文本模式 前两节是本节的理论基础&#xff0c;这节开始代码演示&#xff01; 串口向电脑发送数据 接下来先开始演示一下串口单向发送一个数字给电脑&…

【Git】上传本地文件到Git(以Windows环境为例)

Git 的下载参考&#xff1a;Git 安装及配置 一、Git 上传的整体流程 1、工作区 > 本地仓库 将本地文件上传到Git&#xff0c;需要先上传到本地仓库&#xff0c;然后再上传到远程仓库。要上传文件到本地仓库&#xff0c;不是直接拷贝进去的&#xff0c;而是需要通过命令一步…

2024-02-11 Unity 编辑器开发之编辑器拓展2 —— 自定义窗口

文章目录 1 创建窗口类2 显示窗口3 窗口事件回调函数4 窗口中常用的生命周期函数5 编辑器窗口类中的常用成员6 小结 1 创建窗口类 ​ 当想为 Unity 拓展一个自定义窗口时&#xff0c;只需实现继承 EditorWindow 的类即可&#xff0c;并在该类的 OnGUI 函数中编写面板控件相关的…