嗨,大家好,
让我们深入探讨一下 Jetpack Compose 的一章,在这章里,我们将专注于通过减少不必要的重组来优化性能。很多人分享了通过副作用、将 lambda 表达式作为参数传递等手段以及其他技巧来最小化重组的方法。
但这里有一个更厉害的地方经常被忽视——我们将探讨一视图一状态模式,这能显著减少重组。
准备好看看这怎么玩了吗?那我们就开始吧!🚀
首先,我们来看看通常是如何实现一个特性的。我会用一个简单的例子——一个带有基本UI元素的计时器来演示。
我们将创建一个活动和一个 ViewModel(这只是个例子,不一定符合最佳实践)。
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val mainViewModel = hiltViewModel<MainViewModel>()
val state by mainViewModel.state.collectAsState()
LaunchedEffect(Unit) {
mainViewModel.processAction(MainAction.ContinueData)
}
Data(state)
}
}
}
@Composable
fun Data(state: MainState) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(vertical = 16.dp)
.fillMaxSize()
) {
ContinueUpdateText(state.text)
PlayerA(state.playerAText, state.listA)
PlayerB(state.playerBText, state.listB)
PlayerC(state.playerCText, state.listC)
}
}
@Composable
fun ContinueUpdateText(value: String) {
Text(text = value, fontSize = 12.sp, color = Color.Black)
}
@Composable
fun PlayerA(value: String, list: List<Int>) {
LaunchedEffect(value) {
Log.d("PlayerA", "Recomposed with value: $value, $list")
}
Text(text = value, fontSize = 12.sp, color = Color.Magenta)
}
@Composable
fun PlayerB(value: String, list: List<Int>) {
LaunchedEffect(value) {
Log.d("PlayerB", "Recomposed with value: $value, $list")
}
Text(text = value, fontSize = 12.sp, color = Color.Magenta)
}
@Composable
fun PlayerC(value: String, list: List<Int>) {
LaunchedEffect(value) {
Log.d("PlayerC", "Recomposed with value: $value, $list")
}
Text(text = value, fontSize = 12.sp, color = Color.Magenta)
}
@HiltViewModel
class MainViewModel @Inject constructor() : BaseViewModel<MainState, MainAction>() {
override fun processAction(action: MainAction) {
when (action) {
MainAction.ContinueData -> {
viewModelScope.launch {
var counter = 0
while (true) {
counter = ++counter
if (counter % 2 == 0) {
setState(
getValue().copy(
playerAText = "A-${counter}",
listA = getValue().listA + counter
)
)
}
if (counter % 3 == 0) {
setState(
getValue().copy(
playerBText = "B-${counter}",
listB = getValue().listB + counter
)
)
}
if (counter % 5 == 0) {
setState(
getValue().copy(
playerCText = "C-${counter}",
listC = getValue().listC + counter
)
)
}
setState(
getValue().copy(
text = counter.toString()
)
)
delay(1000)
}
}
}
}
}
override fun initialState(): MainState =
MainState("", "A-0", "B-0", "C-0", true, emptyList(), emptyList(), emptyList())
}
data class MainState(
val text: String,
val playerAText: String,
val playerBText: String,
val playerCText: String,
val shouldVisible: Boolean,
val listA: List<Int>,
val listB: List<Int>,
val listC: List<Int>
)
sealed interface MainAction {
data object ContinueData : MainAction
}
输出:
说明一下:-
- 从这个输出中,你将会注意到重组过程正在所有组件中进行。
很多人可能认为让列表更稳定就能解决我们的问题。
那我们就试试看,让这个列表更稳定吧。
@稳定的
data class ImmutableList<T>(
val items: List<T> // 不可变列表
)
我们将使用 ImmutableList
数据类,而不是我们平常使用的 List
接口。让我们来看看这么改了之后的结果。
输出:
解释:
- 使用
ImmutableList
确实有助于减少某些重新组合过程,但我们只有创建一个新的ImmutableList
才能看到效果。 - 当然,还有其他可能的场景。
这说明虽然不可变列表有所帮助,但要想有效优化还需要考虑更多因素。
这里的问题是:为什么当 PlayerB 的 composable 有变化时,PlayerA 却跳过了重组?还有,你看到了吗?Data
composable 仍在重组。
为什么我们不能让它们完全分开,让Composable A完全不知道Composable B?
那不是更能解决我们重组问题的办法吗?
这就是我所说的一眼一态模式。
让我们来看看这段代码在实际操作中是怎么工作的。
@HiltViewModel 注解
class MainViewModel @Inject constructor() : BaseViewModel<MainState, MainAction>() {
override fun processAction(action: MainAction) {
when (action) {
MainAction.ContinueData -> {
// 当接收到 MainAction.ContinueData 操作时,执行以下操作:
viewModelScope.launch {
var counter = 0
while (true) {
counter = ++counter
if (counter % 2 == 0) {
setState(
getValue().copy(
playerAData = getValue().playerAData.copy(
text = "A-${counter}",
list = getValue().playerAData.list + counter
)
)
)
}
if (counter % 3 == 0) {
setState(
getValue().copy(
playerBData = getValue().playerBData.copy(
text = "B-${counter}",
list = getValue().playerBData.list + counter
)
)
)
}
if (counter % 5 == 0) {
setState(
getValue().copy(
playerCData = getValue().playerCData.copy(
text = "C-${counter}",
list = getValue().playerCData.list + counter
)
)
)
}
setState(
getValue().copy(
text = counter.toString()
)
)
delay(1000)
}
}
}
}
}
override fun initialState(): MainState =
MainState(
"",
PlayerAData.initialData(),
PlayerBData.initialData(),
PlayerCData.initialData()
)
}
数据类 MainState(
val text: String,
val playerAData: PlayerAData,
val playerBData: PlayerBData,
val playerCData: PlayerCData,
) {}
数据类 PlayerAData(
val text: String,
val list: List<Int>
) {
伴生对象 {
fun initialData(): PlayerAData = PlayerAData("A-0", emptyList())
}
}
数据类 PlayerBData(
val text: String,
val list: List<Int>
) {
伴生对象 {
fun initialData(): PlayerBData = PlayerBData("B-0", emptyList())
}
}
数据类 PlayerCData(
val text: String,
val list: List<Int>
) {
伴生对象 {
fun initialData(): PlayerCData = PlayerCData("C-0", emptyList())
}
}
密封接口 MainAction {
数据对象 ContinueData : MainAction
}
- 在这里,我们为不同的可组合项创建了三个对应的数据类(data class),并相应地更新了状态。
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val mainViewModel = hiltViewModel<MainViewModel>()
LaunchedEffect(key1 = Unit) {
mainViewModel.processAction(MainAction.ContinueData)
}
Data()
}
}
}
@Composable
fun Data() {
Column(
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(vertical = 16.dp)
.fillMaxSize()
) {
ContinueUpdateText()
PlayerA()
PlayerB()
PlayerC()
}
}
@Composable
fun ContinueUpdateText() {
val mainViewModel = hiltViewModel<MainViewModel>()
val text by mainViewModel.state.map { it.text }.collectAsState(initial = "")
Text(text = text, fontSize = 12.sp, color = Color.Black)
}
@Composable
fun PlayerA() {
val mainViewModel = hiltViewModel<MainViewModel>()
val playerAData by mainViewModel.state.map { it.playerAData }.collectAsState(initial = PlayerAData.initialData())
LaunchedEffect(key1 = playerAData.text) {
Log.d("PlayerA", "重新组合的值为:${playerAData.text}, ${playerAData.list}")
}
Text(text = playerAData.text, fontSize = 12.sp, color = Color.Magenta)
}
@Composable
fun PlayerB() {
val mainViewModel = hiltViewModel<MainViewModel>()
val playerBData by mainViewModel.state.map { it.playerBData }.collectAsState(initial = PlayerBData.initialData())
LaunchedEffect(key1 = playerBData.text) {
Log.d("PlayerB", "重新组合的值为:${playerBData.text}, ${playerBData.list}")
}
Text(text = playerBData.text, fontSize = 12.sp, color = Color.Magenta)
}
@Composable
fun PlayerC() {
val mainViewModel = hiltViewModel<MainViewModel>()
val playerCData by mainViewModel.state.map { it.playerCData }.collectAsState(initial = PlayerCData.initialData())
LaunchedEffect(key1 = playerCData.text) {
Log.d("PlayerC", "重新组合的值为:${playerCData.text}, ${playerCData.list}")
}
Text(text = playerCData.text, fontSize = 12.sp, color = Color.Magenta)
}
输出:
解释:
- 单个重组:每个可组合项会根据其状态更新独立重组。这确保了一个可组合项的变化不会触发其他可组合项不必要的重组。
- 无需跳过重组:采用这种方法,就不需要跳过重组了。每个可组合项高效地管理自己的状态。
- 父可组合项稳定性:父可组合项(
Data
)在子可组合项发生变化时不会进行重组。每个子可组合项都是隔离的。 - 状态映射:我们利用
map
运算符为每个可组合项映射状态。这创建了一个新的流,只观察每个可组合项相关的状态部分。 - 高效观察:通过为每个可组合项提供一个新的流,我们确保只观察必要的状态变化,从而提高性能并减少不必要的更新。
如果有任何问题,可以在评论里留言,我会尽快回复你,ASAP。我们很快就会深入探讨 Jetpack Compose。
祝你编程开心,直到那时!