目录
在安卓应用中,除了UI之外,另一个关键就是数据的处理。对于Compose UI来说,数据的处理和UI的渲染是天然分开的。比如,使用remember()获取一个MutableState对象,这个数据变化之后,会有机制通知使用这个对象的Compose函数进行更新。
如果自己创建了一个非MutableState对象,这个对象的改变,不会引发Compose UI的重组,表现在界面就是界面不会有任何反应。可以说,MutableState就是Compose中数据的“双向绑定”。
如果是不变数据,直接在代码中进行声明即可。如果是可变数据,则必须使用MutableState。
一、MutableState可变状态
MutableState是Compose中状态管理的基础,当MutableState.value被更新时,MutableState会通知使用过这个对象的Compose组件进行重组,即进行页面刷新。当MutableState.value是自定义对象时,对他更新需要一个对象整个更新,即MutableState.value=newValue,不能使用MutableState.value.changeStauts这种改变状态的方法,不然不会通知Compose组件进行重组。
MutableState通知Compose组件进行数据更新时一种单向流数据,即MutableState数据 => Compose组件。如果Compose组件想要更新MutableState,需要使用事件的方式对MutableState数据进行更新,即MutableState数据 <= Compose组件。
二、数据缓存
可变数据的分层:
1、Composition Scope,即组合作用域,也可以理解为Compose函数这一级的作用域。代表功能是remember函数。
2、Activity/Fragment Scope,可以理解为页面容器这一级的作用域。代表功能是ViewModel。
3、Application Scope,即应用作用域。可以将数据以文件、数据库、远程存储等方式进行存取,即可实现应用级别的数据作用域,或者也可以采用第三方框架。
一)remember
1、介绍
remember是缓存机制。当Compose组件进行重组时会重新执行代码,所以MutableState需要被包含在remember()函数中,remember()可以保证Compose的多次重组中,使用的对象是同一个MutableState,而不是每次重组都重新创建一遍MutableState对象。
remember缓存的状态可以跨重组存在,但是不能跨Activity或者跨进程存在。可以把remember创建的对象提到上级的Compose,进而在多个Compose中引用同一个缓存对象。
2、代码示例
代码示例:
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun RememberCompose() {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
var mutableState = remember { mutableStateOf("A") }
Text(
text = mutableState.value
)
Button(
onClick = {
mutableState.value = "A"
}
) {
Text("设置为A")
}
Button(
onClick = {
mutableState.value = "B"
}
) {
Text("设置为B")
}
}
}
@Preview
@Composable
fun PreRememberCompose() {
RememberCompose()
}
效果:
备注:rememberSavable缓存的状态可以跨Activity或者跨进程存在,当手动退出应用时,缓存数据才会被清空。实现原理就是把数据以Bundle的形势进行保存。类似这类API就不详细介绍了,可以根据需求在网络上寻找对应的API。
3、复杂的UI状态和逻辑
在Compose中定义一个remember对象的方式,适合少量UI状态,简单的UI逻辑。
对于一些较为复杂的UI状态和逻辑,可以使用一个类将相关状态整合到一起,这个叫StateHolder。这种方式需要注意要对MutableState.value进行赋值更新,否则会无法更新Compose UI。
二)ViewModel
viewModel()会从ViewModelStore中获取实例,如果没有实例,就创建一个并存入ViewModelStore。ViewModelStore可能是Activity或者Fragment。
定义ViewModel的方式就是定义一个类,然后继承ViewModel。然后可以用viewModel()函数获取这个类的实例。
代码:
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
class CounterViewModel : ViewModel() {
private val _counter = mutableStateOf(0)
val counter: State<Int> = _counter
fun increment() {
_counter.value = _counter.value + 1
}
fun decrement() {
if (_counter.value > 1) {
_counter.value = _counter.value - 1
}
}
}
@Composable
fun CounterScreen() {
var viewModel: CounterViewModel = viewModel()
CounterComponent(
viewModel.counter,
viewModel::increment,
viewModel::decrement
)
}
@Composable
fun CounterComponent(x0: State<Int>, x1: () -> Unit, x2: () -> Unit) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Text(
text = "value: ${x0.value}"
)
Button(
onClick = x1
) {
Text("increment")
}
Button(
onClick = x2
) {
Text("decrement")
}
}
}
@Preview
@Composable
fun PreCounterScreen() {
CounterScreen()
}
效果:
三)Http请求
首先定义Http的普通类定义:
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.client.statement.bodyAsText
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
suspend fun main() {
println(Http.visit("http://www.bai1du.com"))
}
object Http {
suspend fun visit(url: String): String {
var httpClient = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
})
}
engine {
requestTimeout = 15000
}
}
var errorMessage = ""
try {
var response = httpClient.get(url) {
headers {
append(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
)
}
}
if (response.status.value == 200) {
val content = response.bodyAsText()
println("百度首页内容长度: ${content.length}")
var take = content.take(100)
println("前100个字符: $take")
return take
} else {
println("请求失败,状态码: ${response.status}")
errorMessage += "请求失败,状态码: ${response.status}\n"
}
} catch (e: Exception) {
println("visit $url error!")
errorMessage += "message: ${e.message}\n"
errorMessage += "toString: ${e.toString()}\n"
e.printStackTrace()
} finally {
httpClient.close()
}
return "http get error!\n$errorMessage"
}
}
然后定义ViewModel:
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class UrlViewModel : ViewModel() {
private val _urlData = mutableStateOf(UrlData())
val urlData: State<UrlData> = _urlData
data class UrlData(
var url: String = "http://www.baidu.com",
var content: String = ""
)
fun fetchData() {
viewModelScope.launch {
var url = _urlData.value.url
_urlData.value = UrlData(
url,
Http.visit(url)
)
}
}
fun updateUrl(url: String){
_urlData.value = UrlData(
url,
""
)
}
fun clearContent() {
var url = _urlData.value.url
_urlData.value = UrlData(
url,
""
)
}
}
最后编写UI代码:
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import cn.yy.httptest.ui.theme.HttpTestTheme
@SuppressLint("StateFlowValueCalledInComposition")
@Composable
private fun AppCompose() {
var viewModel: UrlViewModel = viewModel()
UrlView(
viewModel.urlData,
viewModel::updateUrl,
viewModel::fetchData,
viewModel::clearContent
)
}
@Composable
fun UrlView(
urlData: State<UrlViewModel.UrlData>,
updateUrl: (String) -> Unit,
fetchData: () -> Unit,
clearContent: () -> Unit
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Text(
text = "url: ${urlData.value.url}"
)
TextField(
value = urlData.value.url,
onValueChange = updateUrl
)
Button(
onClick = {
fetchData()
}
) {
Text("访问")
}
Button(
onClick = {
clearContent()
}
) {
Text("clear content")
}
Text(
text = "content: ${urlData.value.content}"
)
}
}
@Preview(showBackground = true)
@Composable
fun AppComposePreview() {
HttpTestTheme {
AppCompose()
}
}
效果:
这里是通过ViewModel的方式获取http网站数据,也可以用这种方式获取文件数据等。
三、总结
Compose UI的数据管理有两个关键点:
1、MutableState状态的保存及更新。
2、remember、ViewModel等方式的不同范围的缓存机制。