Android---Jetpack Compose学习003

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

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

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

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

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

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

\bullet 博文和相关评论;

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

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

无状态组件

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

Android---Jetpack Compose学习003_第1张图片

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


    JetpackComposeState
    Expand
    Collapse
    Crop
    Done
    Event
    Privacy
    Restore

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
){
    // 一列多行的布局,上面是又给列表,下面是一个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

Android---Jetpack Compose学习003_第2张图片

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 以显示新状态。

Android---Jetpack Compose学习003_第3张图片

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

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

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

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

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

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

单向数据流

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

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

Android---Jetpack Compose学习003_第4张图片

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 = _name

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

}

class HelloComposeStateActivityWithViewModel : ComponentActivity() {

    // 创建一个 ViewModel
    private val helloViewModel by viewModels()
    // 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

Android---Jetpack Compose学习003_第5张图片

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

\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,
    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())

    val todoItems : LiveData> = _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()

    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 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 中的状态。完成操作后,会创建如下所示的单向数据流设计:

Android---Jetpack Compose学习003_第6张图片

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,
     * 它是 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()

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

Android---Jetpack Compose学习003_第7张图片

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

Android---Jetpack Compose学习003_第8张图片

这个函数使用 remember 给自己添加内存,然后在内存中存储一个由 mutableStateOf 创建的 MutableState,它是 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(或其他有状态对象)时,请务必对其执行 remember 操作,否则它会在每次重组时重新初始化

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

Android---Jetpack Compose学习003_第9张图片

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

修改 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,
     * 它是 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 并将其索引用作键。

你可能感兴趣的:(#,Jetpack,Compose,kotlin,android)