RicardoJiang / wanandroid-compose

Compose+MVI+Navigation实现wanAndroid客户端
450 stars 78 forks source link

关于将 state 全部封装到一个类中有一个疑问 #4

Closed equationl closed 2 years ago

equationl commented 2 years ago

大佬在文中提到

MVVM 的 ViewModle 中分散定义了多个 State ,MVI 使用 ViewState 对 State 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码

并且示例代码也是写的:

 /**
* 页面所有状态
**/
data class LoginViewState(
    val account: String = "",
    val password: String = "",
    val isLogged: Boolean = false
)

并且通过如下代码订阅了这个状态:

    var viewStates by mutableStateOf(LoginViewState())
        private set

我的疑问是,使用 mutableStateOf 订阅了整个 viewState 类,那么如果 viewState 中任意一个属性参数发生变化,都会导致使用到了这个 viewState 的 composable 发生重组?还是说,只会重组使用了改变了的属性的 composable ?

例如上述 viewState 我改变了 account 属性,它会重组所有用到 viewState 的 composable 还是只重组使用了 viewState.account 的 composable ?

如果是重组所有的 composable ,是否会影响到性能?以及这样的话在某些逻辑处理上会造成“死循环”。

能否将不同的状态分开成不同的 viewState ?但是这样的话就不符合您说的

MVVM 的 ViewModle 中分散定义了多个 State ,MVI 使用 ViewState 对 State 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码

equationl commented 2 years ago

在大佬的另外一篇文章找到了解答,对于 livedata 可以使用 distinctUntilChanged 来解决,那 compose 怎么解决这个问题呢?

image

RicardoJiang commented 2 years ago

Compose组件只会在他使用的state发生变化了之后才会发生重组,这是通过Compose编译器插件实现的,所以我们不需要做什么工作,Compose本就支持这种局部刷新

equationl commented 2 years ago

可能是我的表述有误,我的意思是将所有状态写进同一个 data class 后用 mutableState 监听,会导致只要这个 data class 的任意一个属性变化都会导致所有引用了这个 data class 的地方被调用,即使按照 compose 机制检测到虽然被调用了,但是值没有变,所以不会重组,但是实际还是调用了这处代码。

我的问题就是,我会使用某些状态判断是否需要请求服务器,如果发生上述情况,会造成多次重复请求。

例如:

我有一个登录页面,有三个状态:user、psw、isLogging,其中 userpsw 都是 EditText 的值,isLogging 用于判断是否需要请求,所以只要用户输入了内容,保存状态的 data class 都会不停的变化, 这就会导致使用 isLogging 判断的代码也会被不停调用,如果此时恰好 isLogging 为 true, 那么就会不停的向服务器发送请求。

可能我上面举的例子不太恰当,但是我想说的就是上面这个意思。

那么对于这种情况应该怎么规避?把 isLogging 提出来单独作为一个 mutableState 吗?还是就直接从代码逻辑上避免出现这种问题?

equationl commented 2 years ago

附上能够复现这个情况的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 都调用。

equationl commented 2 years ago

为了避免误解,把 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")
    }
}

依旧符合上述所说的情况

RicardoJiang commented 2 years ago

Button3 函数中传入 State3就可以了,没必要传入ViewState Compose重组是通过判断输入的参数有没有发生变化决定的,因为ViewState发生了更新,所以上面的都会重组,应该只传入你需要的参数。

equationl commented 2 years ago

额....大佬好像没理解我的意思

即使按照大佬的意见改为:

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 吗?还是就直接从代码逻辑上避免出现这种问题?

equationl commented 2 years ago

额....好像是我的问题,没理解 compose 的重组机制。

我试了一下,即使把所有 composable 的状态都单独作为一个 mutableState 也会出现上述情况