无状态组件

非结构化状态

在安卓中状态会根据事件进行更新,事件是从应用程序的外部输入。例如用户点击按钮

  • 事件- 事件由用户或者程序的其他部分组成
  • 更新状态-事件处理程序更改UI使用的状态
  • 显示状态-更新UI显示新的状态
    当我们添加更多事件和状态时,可能会出现几个问题
  • 测试:由于UI的状态与Views代码交织在一起,因此很难测试代码。
  • 部分状态更新:当屏幕有更多事件时,很容易忘记更新部分状态以响应事件,因此,用户可能会看到不一致或者不正确的UI。
  • 部分UI更新 :由于我们每次状态更改后手动更新UI,因此有时很容易忘记这一点。因此,用户可能会在其UI中看到随机更新的陈旧数据。
  • 代码复杂性:在这种模式下编码时很难提取一些逻辑。结果,代码变得难以阅读和理解的趋势。

单向数据流

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

Compose的状态管理

状态提升
  • 如果可组合项是无状态的,那它如何才能显示可修改的列表,为了达到这个目的,会使用一种称为状态提升(将状态放到ViewModel中)的技术。
  • Compose中的状态提升是一种将状态移到可组合想的调用方使可组合想无状态的模式,无状态组件更容易测试,可提供更多的重用机会。
class TodoViewModel : ViewModel() {
    private var _todoItems = MutableLiveData(listOf<TodoItem>())
    var todoItems: LiveData<List<TodoItem>> = _todoItems
    fun addItem(item: TodoItem) {
        _todoItems.value = _todoItems.value!! + listOf(item)

    }

    fun removeItem(item: TodoItem) {
        _todoItems.value = _todoItems.value!!.toMutableList().also {
            it.remove(item)
        }
    }
}
@Composable
fun TodoScreen(
    items: List<TodoItem>,
    onAddItem: (TodoItem) -> Unit,
    onRemoveItem: (TodoItem) -> Unit
) {
    Column {
        LazyColumn(
            modifier = Modifier.weight(1f),
            contentPadding = PaddingValues(top = 8.dp)
        ) {
            items(items) {
                TodoRow(
                    todo = it,
                    onItemClicked = { onRemoveItem(it) },
                    modifier = Modifier.fillParentMaxWidth()
                )
            }
        }
        Button(
            onClick = { onAddItem(genrateRandomTodo()) },
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth()
        ) {
            Text(text = "Add random Item")
        }
    }
}

@Composable
fun TodoRow(
    todo: TodoItem,
    onItemClicked: (TodoItem) -> Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier
            .clickable { onItemClicked(todo) }
            .padding(horizontal = 16.dp, vertical = 8.dp),
        horizontalArrangement = Arrangement.SpaceBetween //子元素水平均匀分布
    ) {
        Text(text = todo.taskval)

        Icon(
            imageVector = todo.icon.imageVector,
            contentDescription = stringResource(id = todo.icon.contentDescription)
        )
    }
}
data class TodoItem(
    var taskval: String,
    var icon: TodoIcon = TodoIcon.Default,
    var id: UUID = UUID.randomUUID()
)

enum class TodoIcon(
    val imageVector: ImageVector,
    @StringRes val contentDescription: Int
) {
    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
    }
}
class MainActivity : ComponentActivity() {
    private val todoViewModel by viewModels<TodoViewModel>();
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            JetpackComposeBasicsTheme {
                TodoActivity()
            }
        }
    }

    @Preview
    @Composable
    private fun TodoActivity() {
//    var items = listOf(
//        TodoItem("learn compose", TodoIcon.Event),
//        TodoItem("Take the codelab"),
//        TodoItem("Apply state", TodoIcon.Done),
//        TodoItem("Build dynamic UIS", TodoIcon.Square),
//    )
        val items: List<TodoItem> by todoViewModel.todoItems.observeAsState(listOf())
        TodoScreen(
            items = items,
            onAddItem = { todoViewModel.addItem(it) },
            onRemoveItem = { todoViewModel.removeItem(it) }
        )
    }
}

事实证明,这些参数的组合使得调用方能够从可组合项中提升状态

  • 事件 当用户请求添加和删除项时,TodoScreen会调用onAdd或者onRemove
  • 更新状态 TodoScreen得调用方可以通过更新状态来响应这些事件
  • 显示状态 状态更新后,系统将使用新的items再次调用TodoScreen,而且后者可以在界面上显示他们

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

  • 使用remember存储对象的可组合项会创建内部状态,使得可组合项有状态
  • 在调用方不需要控制状态,并且不必自行管理状态便可使用状态的情况下,“有状态”会非常有用但是,具有内部状态的可组合项往往不易重复使用,也更难测试。
  • 无状态可组合项指不保持任何状态的可组合项。实现无状态的一种简单方法是使用状态提升。

Compose的状态恢复

在重新创建Activity或进程后,可以使用rememberSaveable恢复界面状态。rememberSaveable可以在重组后保持状态。此外,rememberSaveable也可以在重新创建activity或进程后保持吃状态。
存储状态的方式
添加到Bundle的所有数据类型都会自动保存。如果要保存无法添加到Bundle的内容,有一下几种选择:

  • Parcelize:最简单的解决方案是向对象添加@Parcelize注解。对象将变为可打包状态并且可以捆绑。
  • MapSaver:如果某种原因导致@Parcelize不合适,可以使用mapSaver定义自己的规则,规定如何将对象转换为系统可保存到Bundle的一组值。
  • ListSaver:为了避免需要为映射定义键,也可以使用listSaver并将其索引用作键。
import android.os.Bundle
import android.os.Parcelable
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.learn.compose01.ui.theme.JetpackComposeBasicsTheme
import kotlinx.android.parcel.Parcelize

class StateRecoveryParcelableActivity : ComponentActivity() {
    @Parcelize
    data class City(val name: String, val country: String) : Parcelable

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JetpackComposeBasicsTheme {
                CityScreen()
            }
        }

    }

    @Preview
    @Composable
    private fun CityScreen() {
        val (city, setCity) = rememberSaveable {
            mutableStateOf(City("London", "UK"))
        }
//        val (city, setCity) = remember {
//            mutableStateOf(City("London", "UK"))
//        }
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.padding(10.dp)
        ) {
            //点击按钮修改City信息
            TextButton(
                colors = ButtonDefaults.buttonColors(),
                onClick = { setCity(City("Beijing", "CHINA")) }
            ) {
                Text(text = "Clock to change")
            }
            Text(text = "${city.country}-${city.name}")
        }
    }
}
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.learn.compose01.ui.theme.JetpackComposeBasicsTheme

class StateRecoveryMapSaverActivity : ComponentActivity() {
    data class City(val name: String, val country: String)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JetpackComposeBasicsTheme {
                CityScreen()
            }
        }

    }

    @Preview
    @Composable
    private fun CityScreen() {
        //指定mapSaver,用于存储时如何构建一个map,获取时如何构建一个City对象
        val citySaver = run {
            val nameKey = "name"
            val countryKey = "country"
            mapSaver(
                save = {
                    mapOf(
                        nameKey to it.name,
                        countryKey to it.country
                    )
                },
                restore = {
                    City(
                        name = it[nameKey] as String,
                        country = it[countryKey] as String
                    )
                }
            )
        }
        val (city, setCity) = rememberSaveable(stateSaver = citySaver) {
            mutableStateOf(City("London", "UK"))
        }

        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.padding(10.dp)
        )
        {
            //点击按钮修改City信息
            TextButton(
                colors = ButtonDefaults.buttonColors(),
                onClick = { setCity(City("Beijing", "CHINA")) }
            ) {
                Text(text = "Clock to change")
            }
            Text(text = "${city.country}-${city.name}")
        }
    }
}
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐