Compose:1.5 无状态与状态提升(State Hoisting)

文章目录

  • 无状态与状态提升
  • [List 状态更新](#List 状态更新)
    • [List 状态更新解决方案:利用 recompose 重组作用域](#List 状态更新解决方案:利用 recompose 重组作用域)
    • [List 状态更新解决方案:替换数据源对象](#List 状态更新解决方案:替换数据源对象)
    • [List 状态更新解决方案:mutableStateListOf / mutableStateMapOf](#List 状态更新解决方案:mutableStateListOf / mutableStateMapOf)
  • 总结

无状态与状态提升

Compose 它是无状态的(Stateless),这里指的 "状态" 可以认为是属性,例如 TextView 我们可以通过 getText()/setText() 设置 text 属性,而 在 Compose 通过 Text(name) 设置了 name 后,你会发现通过 Text() 拿不到 name 的,这就是所谓的无状态。它是 Compose 的一个特点,并不是一个功能 。但需要注意的是,无状态说的是 Composable 可以无状态,而不是不能有状态

java 复制代码
setContent {
	Hello()
}

@Composable
fun Hello() {
	// text 对 Hello() 而言就是属性,也就是 text 是 Hello() 的内部状态
	// text 对于 Text() 而言是外部状态
	var text = "Hello"
	Text(text)
}

text 对于 Hello() 就是一个属性,一个仅仅它自己可见的变量,你可以说现在 Hello() 它是有状态的,text 是 Hello() 的内部状态。

但在 Hello() 外部还是不能获取到 text,因为它是 Hello() 函数的局部变量,Hello() 函数外部是拿不到的。

所以对于无状态其实还需要做进一步的解释:内部无状态

比如 Text() 无法拿到 text 给它的数值,但是 Text() 外部(即 Hello() 函数内)text 是能获取到的,text 是 Text() 的外部状态。

那么如果想拿 Hello() 函数的 text,就是把 text 从内部状态变成外部状态,也就是 text 变成参数公开给外部访问:

java 复制代码
setContent {
	Box {
		var text = "Hello" // text 要让外部也可访问,将它从 Hello() 抽离出来
		Hello(text)
	}
}

@Composable
fun Hello(text: String) {
	Text(text)
}

这种将内部状态改成外部状态可以公开给外部访问的操作称为状态提升(State Hoisting)

当然这种操作并不是让你将一个变量变成一个全局变量,而是将变量提到一个合适的外部,状态尽量不往上提,这样能尽可能的降低出错的概率。比如上面的 demo 只想 text 在 Box() 内可访问,Box() 外部是不可访问的。

状态提升在业务场景应该怎么很好的使用呢?我们通过 TextField 了解下:

java 复制代码
setContent {
	var name by remember { mutableStateOf("") }
	TextField(name, { 
		// 要手动修改 name 才会更新输入框的内容
		name = it 
	})
}

TextField 相当于 EditText 输入框,它是 material 包下的,foundation 的是 BasicTextField。

可以看到 name 作为状态提升到外层,按照我们的想法,既然是输入框那我输入文本就应该自动改变,但 Compose 并没有为我们这么做,而是要我们自己将修改 name 的操作手动在一个函数类型的 lamba 实现。

看着挺麻烦的,其实这提供了更多的可操作性,name 不会被 TextField() 内部修改导致信息源不单一,通过将交互操作也暴露出来统一让上层处理,在复杂的 Composable 能很好的对状态统一处理,也满足无状态的特点。

所以 对于有交互功能的 Composable,除了要将状态外提,还需要将操作也外提,这样才是完整的状态提升。例如上面的 TextField() 除了 name 状态提升了,输入操作用函数类型的参数也暴露出来提升状态,由程序员自己处理 name,这样才是完整的状态提升。

List 状态更新

java 复制代码
var numbers by mutableStateOf(mutableListOf(1, 2, 3))
setContent {
	Column {
		Button(onClick = { numbers.add(numbers.last() + 1) }) {
			Text("加 1")
		}
		for (number in numbers) {
			Text("第 $number 块文字")
		}
	}
}

上面的 UI 是在一个 Column 显示一个按钮和多个文字,按钮点击时 Column 就累加一个文字。但运行上面的结果你会发现并没有发生更新。

导致这个问题的原因是 by mutableStateOf 是代理的 numbers 的 setValue 和 getValue,即 MutableState 是代理的 MutableList 本身的值被替换了,而不是 MutableList 内部元素的变更。Button 点击事件中的 numbers.add() 并不是触发 numbers 的 setValue,不会触发 recompose,所以并不会触发订阅调用了 getValue 的地方更新

要解决上面的问题有三种解决方案。

List 状态更新解决方案:利用 recompose 重组作用域

java 复制代码
var flag by mutableStateOf(1)
setContent {
	Column {
		// flag 的状态更新了,触发 Recompose 更新 Text,也会更新 Column 内的其他 Composable,达到刷新的目的
		Text("$flag", Modifier.clickable { flag++ })
		Button(onClick = { numbers.add(numbers.last() + 1)}) {
			Text("加 1")
		}
		for (number in numbers) {
			Text("第 $number 块文字")
		}
	}
}

第一种解决方案是利用 recompose 重组作用域,添加一个可观察的数据 flag,recompose 时内部状态更新,Column 内的 Composable 也随之 recompose。

如果是 Button 先点击几次列表添加了数据没更新,然后再点击 flag,此时会触发 recompose,随之更新 Column 下的 Composable,之前点击添加到 numbers 的数据就会显示出来,所以这也证实了确实是因为没有 recompose 才没有更新。

List 状态更新解决方案:替换数据源对象

java 复制代码
var numbers by mutableStateOf(mutableListOf(1, 2, 3))
setContent {
    Column {
        Button(onClick = {
            // 或许你会觉得每次都创建对象会有性能问题,但其实性能影响并不大
            // 但是每次都这样写很麻烦
            numbers = numbers.toMutableList().apply {
                add(numbers.last() + 1)
            }
        }) {
            Text("加 1")
        }
        for (number in numbers) {
            Text("第 $number 块文字")
        }
    }
}

既然 by mutableStateOf 是通过 setValue 触发的更新,那 mutableListOf() 对象改变了那也就可以触发更新了。虽然不会影响多少性能,但每次这样写也太麻烦了。

List 状态更新解决方案:mutableStateListOf / mutableStateMapOf

java 复制代码
// 注意这里是 =,不用 by
val numbers = mutableStateListOf(1, 2, 3)
setContent {
    Column {
        Button(onClick = {
            numbers.add(numbers.last() + 1)
        }) {
            Text("加 1")
        }
        for (number in numbers2) {
            Text("第 $number 块文字")
        }
    }
}

val map = mutableStateMapOf(1 to "One", 2 to "Two")
setContent {
    Column {
        Button(onClick = {
            map[3] = "Three"
        }) {
            Text("加 1")
        }
        for ((key, value) in map) {
			Text("$key 对应的是 $value")
		}
    }
}

Compose 提供的 mutableStateListOf(如果使用 Map 就是 mutableStateMapOf),mutableStateListOf 是内部的元素被观测,不是它本身被观测,所以 mutableStateListOf 内部数据更新了就会通知更新

总结

1、无状态和状态提升

  • 无状态说的是 Composable 可以无状态,而不是不能有状态

  • 将内部状态改成外部状态可以公开给外部访问的操作称为状态提升(State Hoisting)

  • 对于有交互功能的 Composable,除了要将状态外提,还需要将操作也外提,做到信息源单一,这样才是完整的状态提升

2、List 状态更新

列表或 Map 的更新不能简单使用 by mutableStateOf,一般是要修改的容器元素,Compose 提供了 mutableStateListOf(如果使用 Map 就是 mutableStateMapOf),mutableStateListOf 是内部的元素被观测,不是它本身被观测,所以 mutableStateListOf 内部数据更新了就会通知更新

相关推荐
xingpanvip1 小时前
星盘接口开发文档:天象盘接口指南
android·开发语言·python·php·lua
天涯海风2 小时前
写一个录音并保存到手机的工具 安卓工具类
android·java·智能手机
黄林晴2 小时前
Koin 开发者炸了!7 条规则根治运行时错误,自动扫描太香了
android
恋猫de小郭2 小时前
Flutter 3.41.8 又双叒修复调试问题,草台班子日常 hotfix
android·前端·flutter
火山上的企鹅2 小时前
QGC 二次开发(RTK):内置 NTRIP Client,实现 CORS 差分数据接入与 GPS_RTCM_DATA 转发
android·无人机·rtk·qgroundcontrol
Kapaseker2 小时前
客官,你误会 Compose Strong Skipping 了
android·kotlin
张风捷特烈2 小时前
状态管理大乱斗#04 | Riverpod 源码评析 (上) - 核心架构
android·前端·flutter
大鹏说大话2 小时前
构造函数属性提升的利与弊:如何优雅地编写价值对象(Value Object)
android
大黄说说3 小时前
匹配表达式 vs. Switch语句:现代PHP中的条件逻辑重构
android·ide·android studio