Android Jetpack Compose状态管理与状态提升

Compose 状态管理

什么是状态

Compose是声明式UI,UI的变化都是由数据的状态驱动的,在了解状态管理之前,先要了解为什么需要状态管理。

Compose界面的构成都是由一个个可组合函数构成的,当可组合函数的输入发生变化时,系统会重新执行这个可组合函数并重新构建UI,例如TextView显示文本,当文本内容发生变化时,随之UI也需要发生变化,下面是状态驱动UI变化的实现:

kt 复制代码
setContent {
    var text = "Hello World"
    StateTest(text) {
        text = "Hello Compose"
    }
}

@Composable
fun StateTest(text: String, onClick: () -> Unit) {
    Text(
        text = text,
        color = Color.Black,
        modifier = Modifier
            .padding(10.dp)
            .fillMaxWidth()
            .clickable {
                onClick()
            }
    )
}

以上代码想要的结果是当点击Text时,Text显示的文本发生改变,但是当我们点击时,发现文本内容并没有改变,此时就需要通过状态来控制Compose函数重组时的UI变化。此时就需要用到rememberSnapshotStateremember是保存一个表达式计算的值,这个值发生变化会导致使用这个值作为输入参数的Compose函数发生重组,snapshotState 是 Compose 状态管理系统的核心机制,它通过快照系统,允许Compose函数根据输入进行自动重组,不再是传统View中需要手动更新。将上面的代码实现为状态管理的方式更新UI,代码如下:

kt 复制代码
setContent {
    val text = "Hello World"
    var rememberText by remember {
        mutableStateOf(text)
    }
    StateTest(rememberText) {
        rememberText = "Hello Compose"
    }
}

@Composable
fun StateTest(text: String, onClick: () -> Unit) {
    Text(
        text = text,
        color = Color.Black,
        modifier = Modifier
            .padding(10.dp)
            .fillMaxWidth()
            .clickable {
                onClick()
            }
    )
}

此时,Text点击事件中,我们改变了rememberText的值,因为这个值是被remember记录的,会触发StateTest函数的重组,UI界面发生变化。

remember函数提供重载的函数有一个至多个参数,

kt 复制代码
@Composable
inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)

@Composable
inline fun <T> remember(
    key1: Any?,
    crossinline calculation: @DisallowComposableCalls () -> T
): T {
    return currentComposer.cache(currentComposer.changed(key1), calculation)
}

可以根据自己的需求传入不同的计算条件参与值的计算,从而根据缓存的值判断Compose函数在数据源发生变化时是否需要重组:

例如我们在Compose函数首次重组时,根据传入的List数据源计算值大于60的数据集合,并在列表项里将大于60的item背景色标记为红色,可以实现如下:

kt 复制代码
@Composable
fun ScoreFlagList(scoreList: MutableList<Int>) {
    val passScoreList = remember(scoreList) {
        scoreList.filter { it > 60 }.map { scoreList.indexOf(it) }.toSet()
    }

    Column {
        scoreList.forEach {
            Text(
                text = "score: $it",
                modifier = Modifier
                    .fillMaxWidth()
                    .height(40.dp)
                    .background(
                        color = if (passScoreList.contains(scoreList.indexOf(it))) {
                            Color.Green
                        } else {
                            Color.Red
                        }
                    )
            )
        }
    }
}
状态管理相关API与用法

Compose函数因为数据变化而发生重组必须是State类型的数据,并且被remember缓存在Compose函数的内存快照中,对于一些可变但不可被观察的数据,例如ArrayList、mutableListOf或可变数据类等,我们必须使用例如MutableState<ArrayList>的形式,让数据能被可组合函数观察到。同时Compose并不是强制要求MutableState存储状态,也可以将其他可观察的数据转为Compose可观察的状态,例如LiveData,Flow等

  • 将可变List集合转换为可观察对象
kt 复制代码
val list = mutableListOf(65, 76, 98, 86, 11, 74, 68, 72, 97, 91)
val stateList =  remember { mutableStateOf(list) }
  • 将LiveData转为状态变量
kt 复制代码
@Composable
fun LiveDataState(textString: MutableLiveData<Int>) {
    val liveData = textString.observeAsState()
    Text(
        text = "Number = ${liveData.value}", modifier = Modifier
            .fillMaxWidth()
            .height(30.dp)
            .clickable {
                textString.value = textString.value!! + 1
            })
}
  • 将Flow数据转换为State变量
kt 复制代码
@Composable
fun FlowDataState(textString: MutableStateFlow<Int>) {
    val liveData = textString.collectAsState(initial = 0)
    Text(
        text = "Number = ${liveData.value}", modifier = Modifier
            .fillMaxWidth()
            .height(30.dp)
            .clickable {
                textString.update { it + 1 }
            })
}

官方还提供了RxJava的数据变化时,转换为State的相关API,同时自定义的一些可观察的数据应该使用produceStateAPI来产生状态。

单向数据流

在传统的View体系中,假设我们需要实现一个Text用于实时显示输入框输入的内容,我们可以这样实现:

kt 复制代码
class MainActivity2 : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.editText.doAfterTextChanged { text ->
            binding.helloText.text = "hello, ${text.toString()}"
        }
    }
}

当输入框数据发生变化时,会回调doAfterTextChanged事件,然后将数据更新到TextView中,在复杂的场景中,一个事件导致一个UI的更新,同时也可能导致另一个UI的更新或者数据变化,这种状态变化称之为非结构化状态,在Android传统开发中,这种非结构化状态无法进行系统性的单元测试,View与事件处理与数据是混合在一起的。因此引入ViewModel和LiveData进行事件、数据和UI的分离。

kt 复制代码
class MainActivity2 : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    private val inputTextViewModel by viewModels<InputTextViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.editText.doAfterTextChanged { text ->
            inputTextViewModel.onInputTextChange(text.toString())
        }

        inputTextViewModel.inputText.observe(this) {
            binding.helloText.text = it
        }
    }
}

class InputTextViewModel : ViewModel() {
    private val _inputText = MutableLiveData<String>()
    val inputText: LiveData<String> = _inputText

    fun onInputTextChange(text: String) {
        _inputText.value = text
    }
}

这样UI的事件流向ViewModel,ViewModel中改变数据,UI监听LiveData,LiveData数据被改变驱动UI更新,这种方式称之为单向数据流。

单向数据流的优点如下:

  • 可预测性与调试简化
  • 状态隔离与安全性
  • 可扩展性提升
状态提升

前面介绍了状态,状态更新一般由事件产生,常见的用户的输入事件,例如点击、长按,或者一些其他事件导致数据变化的事件,此时状态就应该更新,随之更新UI。而Compose最大的特点就是可组合的,我们写的空间要想拥有很强的复用性,我们就需要状态提升,状态提升就是把状态作为参数传入给可组合函数,状态的变化由调用方控制。

kt 复制代码
class ComposeLearnActivity : ComponentActivity() {
    private val inputTextViewModel by viewModels<InputTextViewModel2>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            InputTextPreviewComponent(inputText = inputTextViewModel.inputText, onInputTextChanged = {
                inputTextViewModel.onInputTextChange(it)
            })
        }
    }
}

@Composable
fun InputTextPreviewComponent(inputText: State<String>, onInputTextChanged: (String) -> Unit) {
    Column {
        Text(text = inputText.value)
        TextField(value = inputText.value, onValueChange = onInputTextChanged)
    }
}

class InputTextViewModel2 : ViewModel() {
    private val _inputText: MutableState<String> = mutableStateOf("")
    val inputText: State<String> = _inputText
    fun onInputTextChange(text: String) {
        _inputText.value = text
    }
}

以上代码就是一个状态提升的实例,我们可以看到InputTextPreviewComponent函数内部只处理UI的显示和事件的回调,并没有操作ViewModel的数据,因此复用起来更方便。

状态保存与恢复

Activity在某些情况下可能会销毁重建,例如屏幕旋转没有正确配置configChanges设置,如果Activity销毁重建了,我们还需要保留之前的状态,就需要使用到rememberSaveable将状态保存到Bundle中,在重建的时候读取数据并恢复状态。先看一个不保存状态的例子:

kt 复制代码
class ComposeLearnActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d("Compose", "onCreate")
        setContent {
            val countNUm = remember { mutableStateOf(0) }
            TextCounter(countNUm, {
                countNUm.value++
            })
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("Compose", "onDestroy")
    }
}

@Composable
fun TextCounter(countNUm: State<Int>, clickable: () -> Unit) {
   Row(
       modifier = Modifier
           .fillMaxWidth()
           .fillMaxHeight(),
       verticalAlignment = Alignment.CenterVertically
   ) {
       Text(
           "当前计数:${countNUm.value}",
           modifier = Modifier
               .fillMaxWidth()
               .padding(vertical = 10.dp)
               .background(Color(0xFFE0E0E0))
               .clickable { clickable() },
           textAlign = TextAlign.Center,
           fontSize = 15.sp,
           color = Color(0xFF000000),
       )
  	}
}

Activity重建之后还会执行生命周期方法onCreate 此时TextCounter 会重组,此时的值还是默认值0。此时只需要将remember改为rememberSaveable ,销毁重建之后还是原来的值。

kt 复制代码
val countNUm = rememberSaveable{
                mutableStateOf(0)
            }

rememberSaveable会自动将基础类型的数据存储到Bundle中,如果你需要保存的数据是自定义类型的,比如数据类,你可以使用不同的存储机制,例如使用 Parcelize 注解、使用 listSavermapSaver 等 Compose API,或实现会扩展 Compose 运行时 Saver 类的自定义 Saver 类。

例如使用Parcelize注解:@kotlinx.parcelize.Parcelize是一个gradle插件,需要导入相关插件

kt 复制代码
@kotlinx.parcelize.Parcelize
data class City(var name: String, var score: Int) : Parcelable

在onCreate方法中,我们只需要使用rememberSaveable进行状态保存与恢复即可实现

kt 复制代码
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d("Compose", "onCreate")
    setContent {
        val city = rememberSaveable {
            mutableStateOf(City("上海", 82))
        }
        CityShow(city) {
            city.value = city.value.copy(
                score = city.value.score + 1
            )
        }
    }
}

还有一种方式就是使用Compose提供的Saver,可以看到官方已经实现了一些保存容器状态的Saver,我们要实现自己的自定义Saver可以参考官方的实现。官方文档地址

kt 复制代码
fun <Original, Saveable> listSaver(
    save: SaverScope.(value: Original) -> List<Saveable>,
    restore: (list: List<Saveable>) -> Original?
): Saver<Original, Any> =
    @Suppress("UNCHECKED_CAST")
    Saver(
        save = {
            val list = save(it)
            for (index in list.indices) {
                val item = list[index]
                if (item != null) {
                    require(canBeSaved(item)) { "item can't be saved" }
                }
            }
            if (list.isNotEmpty()) ArrayList(list) else null
        },
        restore = restore as (Any) -> Original?
    )