Jetpack Compose -> 无状态、状态提升,单向数据流

前言


上一章节我们讲解了重组作用域和 remember,本章我们来讲解下 Compose 的 『无状态』,状态提升,单向数据流;

无状态


所谓的无状态(Stateless)的这个状态指得是什么呢?就是控件的各种属性,比如 TextView 它就有一个状态属性,就是它的内容,我们可以通过 getText 和 setText 来分别获取设置它的内容,这个内容就是它的一个状态信息,而 Compose 是无状态的,是没有这种属性的,对应的Compose中的 Text()

var name = "Mars"
Text(text = name)

我们在设置这个name之后,我们是无法从其他地方拿到这个 name 的文字信息的,这个文字参数只是被 Text 拿来设置用来显示之后就扔了,后续想拿到是拿不到的,因为它根本就没有保存,这就是所谓的无状态;

这个无状态只能说是 Compose 这种声明式框架的一个特点,Compose 可以的无状态并不是绝对的,例如我们来看下面这个函数

@Composable
fun say() {
    var text = "Hello"
    Text(text)
}

这个函数中的 Text() 是无状态的,但是这个 say() 函数却是有状态的,它的里面有一个仅仅它自己能看到的字符串 “Hello”;

所以 这里所说的无状态都是指的内部的无状态,例如 say() 这个 Compose 组件,它的内部的 Hello 是有状态的,但是,当我们调用这个 say() 的时候

setContent {
    say()
}

我们在 setContent 中是获取不到 say 的任何状态的;如果我们想获取这个内部状态的状态值应该怎么实现呢?

状态提升


例如我们想获取这个 text 的状态,我们需要将这个 text 放到say() 和 Text() 的外部,

setContent {
    var text = "Hello"
    say(text)
}

@Composable
fun say(value: String) {
    Text(value)
}

这样,我们就可以在 say() 的外部获取到这个状态了;这种在 Compose 中就叫作状态提升(State Hoisting)这个 Hoisting 就是提升,意思就是将状态提升到外部组件中;

同理,我们也只能在 setContent 中拿到这个状态,但是如果我们想在 setContent 的外部获取到这个状态,那么就需要将这个 text 提升到 setContent 的外部;

但是,这种状态提升,要尽量少的提升,最需要的地方提升即可;

这个时候,可能就会有人有疑问了,这种状态提升,不就导致调用麻烦了吗?我只想调用 say() 函数,结果现在需要定义一个变量传递进去,这个其实也好修改,我们可以给 value 定义一个默认值

@Composable
fun say(value: String = "Kobe") {
    Text(value)
}

这样就可以直接调用 say() 方法了;

无状态、状态提升的另一种特殊用法

我们接下来看另一个比较特殊的无状态、状态提升的用法,我们来看另一个组件 TextFiled 文字输入框

TextField(value = , onValueChange = ) //文字输入框

相当于原生的 EditText,它是 material 层的,不是 foundation 层,也不是 ui 层的,跟 Button 类似,是一个符合 material 风格的输入框,如果不想使用 material 风格,可以使用 BasicTextField 自己设计输入框风格;

它有两个参数,一个 value 一个 onValueChange,一个文字参数,一个文字改变监听数据变化,其中这个 value 就是提出来的参数,文字原本是内部的状态,现在提出来了,就成了输入的了,外部输入,就成了无状态的了;

var name
TextField(name , onValueChange = ) //文字输入框

name 作为一个外部变量来充当这个无状态文本输入框的外部状态;

onValueChange 它是一个函数类型的表达式,我们可以写成

var name
TextField(name , {}) //文字输入框

这种形式的,这个函数中要做的就是处理文字变化的事,也就是回调,我们来运行看下效果,这就是 material 风格的输入框

image.png

我们接下来输入几个数据看下:

SVID_20240312_202210_1.gif

可以看到,输入内容之后不显示,这是为什么呢?这是因为用户在输入新的内容之后并没有更新到 name 字段导致的,我们需要做如下更改:

var name = ""
TextField(name , onValueChange ={
    name = it
})

就是说 我们并没有把用户的输入行为和显示来源做关联,这就需要我们进行一个关联才行,但是这还不是我们最终的写法,因为我们是在 Compose 中,所以这些会变的变量我们不能直接写,需要用 mutableStateOf 和 remember 将它包裹起来才行

var name by remember {
     mutableStateOf("")
}
TextField(name , onValueChange = { newValue->
    name = newValue
})

这样,我们的修改才能生效,我们运行看下效果:

SVID_20240312_204027_1.gif

可以看到,我们的输入显示到了输入框中;对于这个 TextFiled Compose 并不会主动帮我们更新,而是需要我们手动更新,那么 Compose 为什么要这么做呢?

单向数据流


带着这个问题,我们先来聊聊数据,当我们既有缓存数据又有网络数据的时候,我们是如何将缓存数据和网络数据进行一个结合呢?第一次打开的时候,本地数据为空,从网络加载到数据之后显示并存到本地数据库,当加载下一页数据的时候,取到下一页数据合并到内存后显示,同时存到数据库,如果用户杀死app,重新打开,优先取数据库的数据同时从网络取数据,然后合并到内存,同时更新数据库;

那么问题来了,在这种双通道取数据的情况下,怎么保证数据的有效性呢?如何保证数据的同步性呢?

解决的本质就是:Single source of Truth(单一数据源),这样就不会出现数据冲突的问题了;让网络数据作为本地数据的上游,这样就不会出现数据不同步的问题了,这种方案也是被 Compose 官方建议的方案;这种单一数据源在 Compose 之前就已经被 Android 使用了,例如 Jetpack ViewModel 中的 Repository,它内部就是『数据库 + 网络』的形式;

Compose 所有会用到的界面数据都是单一数据源的方式,例如我们上面说的 TextFiled

var name by remember {
     mutableStateOf("")
}
TextField(name , onValueChange = { newValue->
    name = newValue
})

我们接着来看这段代码,如果我们想给输入框做一些限制,例如,限制输入框不能输入一些标点符号,那么我们就需要在 onValueChange 的回调中判断 newValue 的值是否符合规范,在不符合规范的情况下将用户输入的内容清除掉;

那么在这种情况下, Compose 就是通过单向数据流的方式来实现用户从 输入- 修改 - 显示 的过程的;整体数据从上往下传输,事件从下往上传输,一层一层的单向数据流传递;

好了,今天的讲解就到这里吧

下一章预告


状态机制的原理

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力~~

  • 14
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要去掉Jetpack Compose Navigation中的NavigationItem的选中状态,可以使用`rememberNavController`来获取NavController,然后使用`composable`函数中的`navController.currentBackStackEntryAsState()`来获取当前的BackStackEntry。接着,你可以使用`remember`函数来创建一个`LocalBackPressedDispatcher`,并在导航项目的点击事件中调用`navController.navigate`来实现导航。最后,在`NavigationItem`周围包装一个`UnselectedContent`组件,用于在该项目未被选中时显示内容。下面是一个示例代码: ``` val navController = rememberNavController() val backPressedDispatcher = remember { LocalBackPressedDispatcherOwner.current.onBackPressedDispatcher } NavHost( navController, startDestination = "home" ) { composable("home") { HomeScreen( navController = navController, onBackPressed = { backPressedDispatcher.onBackPressed() } ) } composable("profile") { ProfileScreen( navController = navController, onBackPressed = { backPressedDispatcher.onBackPressed() } ) } } BottomNavigation { NavigationItem( icon = Icons.Filled.Home, label = "Home", route = "home", navController = navController, backPressedDispatcher = backPressedDispatcher ) NavigationItem( icon = Icons.Filled.Person, label = "Profile", route = "profile", navController = navController, backPressedDispatcher = backPressedDispatcher ) } @Composable fun NavigationItem( icon: ImageVector, label: String, route: String, navController: NavController, backPressedDispatcher: OnBackPressedDispatcher ) { val backstackEntry = navController.currentBackStackEntryAsState() val selected = backstackEntry.value?.destination?.route == route val onClick = { if (!selected) { navController.navigate(route) { popUpTo(navController.graph.startDestinationId) launchSingleTop = true } } } Box( modifier = Modifier .clickable(onClick = onClick) .height(56.dp) .fillMaxWidth(), contentAlignment = Alignment.Center ) { if (!selected) { UnselectedContent(icon = icon, label = label) } else { SelectedContent(icon = icon, label = label) } } } @Composable fun UnselectedContent(icon: ImageVector, label: String) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Icon(icon, contentDescription = null) Text(text = label) } } @Composable fun SelectedContent(icon: ImageVector, label: String) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Icon(icon, contentDescription = null, tint = MaterialTheme.colors.primary) Text(text = label, color = MaterialTheme.colors.primary) } } ``` 在上面的示例代码中,`UnselectedContent`组件用于在导航项目未被选中时显示内容。我们使用了`Box`组件包装了导航项目,然后在`Box`组件中使用了`UnselectedContent`和`SelectedContent`组件来分别显示未选中和选中的状态。如果当前导航项目未被选中,则显示`UnselectedContent`组件。否则,显示`SelectedContent`组件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值