Closed equationl closed 2 years ago
在大佬的另外一篇文章找到了解答,对于 livedata 可以使用 distinctUntilChanged 来解决,那 compose 怎么解决这个问题呢?
Compose组件只会在他使用的state发生变化了之后才会发生重组,这是通过Compose编译器插件实现的,所以我们不需要做什么工作,Compose本就支持这种局部刷新
可能是我的表述有误,我的意思是将所有状态写进同一个 data class
后用 mutableState
监听,会导致只要这个 data class 的任意一个属性变化都会导致所有引用了这个 data class
的地方被调用,即使按照 compose 机制检测到虽然被调用了,但是值没有变,所以不会重组,但是实际还是调用了这处代码。
我的问题就是,我会使用某些状态判断是否需要请求服务器,如果发生上述情况,会造成多次重复请求。
例如:
我有一个登录页面,有三个状态:user、psw、isLogging
,其中 user
和 psw
都是 EditText
的值,isLogging
用于判断是否需要请求,所以只要用户输入了内容,保存状态的 data class
都会不停的变化, 这就会导致使用 isLogging
判断的代码也会被不停调用,如果此时恰好 isLogging
为 true, 那么就会不停的向服务器发送请求。
可能我上面举的例子不太恰当,但是我想说的就是上面这个意思。
那么对于这种情况应该怎么规避?把 isLogging
提出来单独作为一个 mutableState 吗?还是就直接从代码逻辑上避免出现这种问题?
附上能够复现这个情况的demo:
MainActicity.kt
private const val TAG = "test"
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val viewModel: TestViewModel = viewModel()
val viewState = viewModel.viewStates
MyApplicationTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
TestScreen(viewModel, viewState)
}
}
}
}
}
@Composable
fun TestScreen(viewModel: TestViewModel, viewState: TestViewState) {
Column {
Log.i(TAG, "TestScreen: recompose all1")
if (viewState.state1 != "") {
Log.i(TAG, "TestScreen: state1 recompose")
}
else {
Log.i(TAG, "TestScreen: state1 recompose2")
}
if (viewState.state2) {
Log.i(TAG, "TestScreen: state2 recompose")
}
else {
Log.i(TAG, "TestScreen: state2 recompose2")
}
if (viewState.state3 != 0) {
Log.i(TAG, "TestScreen: state3 recompose")
}
else {
Log.i(TAG, "TestScreen: state3 recompose2")
}
Text(text = "state1 ${viewState.state1}")
Text(text = "state2 = ${viewState.state2}")
Text(text = "state3 = ${viewState.state3}")
Button(onClick = {
viewModel.dispatch(TestViewAction.ClickBtn1)
}) {
Text(text = "click1")
}
Button(onClick = {
viewModel.dispatch(TestViewAction.ClickBtn2)
}) {
Text(text = "click2")
}
Button(onClick = {
viewModel.dispatch(TestViewAction.ClickBtn3)
}) {
Text(text = "click3")
}
}
}
TestViewModel.kt
class TestViewModel: ViewModel() {
var viewStates by mutableStateOf(TestViewState())
private set
private val _viewEvents = Channel<TestViewEvent>(Channel.BUFFERED)
val viewEvents = _viewEvents.receiveAsFlow()
fun dispatch(action: TestViewAction) {
when (action) {
is TestViewAction.ClickBtn1 -> clickBtn1()
is TestViewAction.ClickBtn2 -> clickBtn2()
is TestViewAction.ClickBtn3 -> clickBtn3()
}
}
private fun clickBtn1() {
viewStates = viewStates.copy(state1 = "edit")
}
private fun clickBtn2() {
viewStates = viewStates.copy(state2 = !viewStates.state2)
}
private fun clickBtn3() {
viewStates = viewStates.copy(state3 = viewStates.state3 + 1)
}
}
data class TestViewState(
val state1: String = "state1",
val state2: Boolean = false,
val state3: Int = 0
)
sealed class TestViewEvent { }
sealed class TestViewAction {
object ClickBtn1 : TestViewAction()
object ClickBtn2 : TestViewAction()
object ClickBtn3 : TestViewAction()
}
如果运行这个程序,点击任意按钮都会打印四条日志,例如第一次运行时点击按钮1会打印
I/test: TestScreen: recompose all1
I/test: TestScreen: state1 recompose
I/test: TestScreen: state2 recompose2
I/test: TestScreen: state3 recompose2
理想状态当然是点击哪个按钮就重组或调用涉及到 对应 state 的地方,而不是所有 state 都调用。
为了避免误解,把 MainActivity.kt 改为
private const val TAG = "test"
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val viewModel: TestViewModel = viewModel()
val viewState = viewModel.viewStates
MyApplicationTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
TestScreen(viewModel, viewState)
}
}
}
}
}
@Composable
fun TestScreen(viewModel: TestViewModel, viewState: TestViewState) {
Column {
Log.i(TAG, "TestScreen: recompose all1")
Button1(viewState = viewState, viewModel = viewModel)
Button2(viewState = viewState, viewModel = viewModel)
Button3(viewState = viewState, viewModel = viewModel)
}
}
@Composable
fun Button1(viewState: TestViewState, viewModel: TestViewModel) {
if (viewState.state1 != "") {
Log.i(TAG, "TestScreen: state1 recompose")
}
else {
Log.i(TAG, "TestScreen: state1 recompose2")
}
Column {
Text(text = "state1 ${viewState.state1}")
Button(onClick = {
viewModel.dispatch(TestViewAction.ClickBtn1)
}) {
Text(text = "click1")
}
}
}
@Composable
fun Button2(viewState: TestViewState, viewModel: TestViewModel) {
if (viewState.state2) {
Log.i(TAG, "TestScreen: state2 recompose")
}
else {
Log.i(TAG, "TestScreen: state2 recompose2")
}
Column {
Text(text = "state2 = ${viewState.state2}")
Button(onClick = {
viewModel.dispatch(TestViewAction.ClickBtn2)
}) {
Text(text = "click2")
}
}
}
@Composable
fun Button3(viewState: TestViewState, viewModel: TestViewModel) {
if (viewState.state3 != 0) {
Log.i(TAG, "TestScreen: state3 recompose")
}
else {
Log.i(TAG, "TestScreen: state3 recompose2")
}
Text(text = "state3 = ${viewState.state3}")
Button(onClick = {
viewModel.dispatch(TestViewAction.ClickBtn3)
}) {
Text(text = "click3")
}
}
依旧符合上述所说的情况
Button3 函数中传入 State3就可以了,没必要传入ViewState Compose重组是通过判断输入的参数有没有发生变化决定的,因为ViewState发生了更新,所以上面的都会重组,应该只传入你需要的参数。
额....大佬好像没理解我的意思
即使按照大佬的意见改为:
private const val TAG = "test"
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val viewModel: TestViewModel = viewModel()
val viewState = viewModel.viewStates
MyApplicationTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
TestScreen(viewModel, viewState)
}
}
}
}
}
@Composable
fun TestScreen(viewModel: TestViewModel, viewState: TestViewState) {
Column {
Log.i(TAG, "TestScreen: recompose all1")
Button1(viewState.state1, viewModel)
Button2(viewState.state2, viewModel)
Button3(viewState.state3, viewModel)
}
}
@Composable
fun Button1(state: String, viewModel: TestViewModel) {
if (state != "") {
Log.i(TAG, "TestScreen: state1 recompose")
}
else {
Log.i(TAG, "TestScreen: state1 recompose2")
}
Column {
Text(text = "state1 $state")
Button(onClick = {
viewModel.dispatch(TestViewAction.ClickBtn1)
}) {
Text(text = "click1")
}
}
}
@Composable
fun Button2(state: Boolean, viewModel: TestViewModel) {
if (state) {
Log.i(TAG, "TestScreen: state2 recompose")
}
else {
Log.i(TAG, "TestScreen: state2 recompose2")
}
Column {
Text(text = "state2 = $state")
Button(onClick = {
viewModel.dispatch(TestViewAction.ClickBtn2)
}) {
Text(text = "click2")
}
}
}
@Composable
fun Button3(state: Int, viewModel: TestViewModel) {
if (state != 0) {
Log.i(TAG, "TestScreen: state3 recompose")
}
else {
Log.i(TAG, "TestScreen: state3 recompose2")
}
Text(text = "state3 = $state")
Button(onClick = {
viewModel.dispatch(TestViewAction.ClickBtn3)
}) {
Text(text = "click3")
}
}
点击任意按钮,依旧会调用所有 Button 的判断。
这里只是为了说明这个现象,真正的问题点是:
我的问题就是,我会使用某些状态判断是否需要请求服务器,如果发生上述情况,会造成多次重复请求。 例如: 我有一个登录页面,有三个状态:user、psw、isLogging,其中 user 和 psw 都是 EditText 的值,isLogging 用于判断是否需要请求,所以只要用户输入了内容,保存状态的 data class 都会不停的变化, 这就会导致使用 isLogging 判断的代码也会被不停调用,如果此时恰好 isLogging 为 true, 那么就会不停的向服务器发送请求。 可能我上面举的例子不太恰当,但是我想说的就是上面这个意思。 那么对于这种情况应该怎么规避?把 isLogging 提出来单独作为一个 mutableState 吗?还是就直接从代码逻辑上避免出现这种问题?
额....好像是我的问题,没理解 compose 的重组机制。
我试了一下,即使把所有 composable 的状态都单独作为一个 mutableState 也会出现上述情况
大佬在文中提到
并且示例代码也是写的:
并且通过如下代码订阅了这个状态:
我的疑问是,使用
mutableStateOf
订阅了整个viewState
类,那么如果viewState
中任意一个属性参数发生变化,都会导致使用到了这个viewState
的 composable 发生重组?还是说,只会重组使用了改变了的属性的 composable ?例如上述
viewState
我改变了account
属性,它会重组所有用到viewState
的 composable 还是只重组使用了viewState.account
的 composable ?如果是重组所有的 composable ,是否会影响到性能?以及这样的话在某些逻辑处理上会造成“死循环”。
能否将不同的状态分开成不同的
viewState
?但是这样的话就不符合您说的