如何在 Jetpack Compose 中正确使用 StateFlow?
How to properly use StateFlow with Jetpack compose?
我正在 ViewModel 中执行 API 调用并在可组合项中观察它,如下所示:
class FancyViewModel(): ViewModel(){
private val _someUIState =
MutableStateFlow<FancyWrapper>(FancyWrapper.Nothing)
val someUIState: StateFlow<FancyWrapper> =
_someUIState
fun attemptAPICall() = viewModelScope.launch {
_someUIState.value = FancyWrapper.Loading
when(val res = doAPICall()){
is APIWrapper.Success -> _someUIState.value = FancyWrapper.Loading(res.vaue.data)
is APIWrapper.Error -> _someUIState.value = FancyWrapper.Error("Error!")
}
}
}
在组合中,我正在听 'someUIState' 这样的:
@Composable
fun FancyUI(viewModel: FancyViewModel){
val showProgress by remember {
mutableStateOf(false)
}
val openDialog = remember { mutableStateOf(false) }
val someUIState =
viewModel.someUIState.collectAsState()
when(val res = someUIState.value){
is FancyWrapper.Loading-> showProgress = true
is FancyWrapper.Success-> {
showProgress = false
if(res.value.error)
openDialog.value = true
else
navController.navigate(Screen.OtherScreen.route)
}
is FancyWrapper.Error-> showProgress = false
}
if (openDialog.value){
AlertDialog(
..
)
}
Scaffold(
topBar = {
Button(onClick={viewModel.attemptAPICall()}){
if(showProgress)
CircularProgressIndicator()
else
Text("Click")
}
}
){
SomeUI()
}
}
我面临的问题是 FancyUI 可组合项中某些 UIState 的 'when' 块代码在可组合项重组期间被多次触发,即使没有单击脚手架中的按钮(例如:当 AlertDialog 出现时)。我哪里做错了?在 Composable 中使用 StateFlow 观察数据的正确更好方法是什么?
如果你只想处理每个 someUIState
值一次,你应该把它放在一个 LaunchedEffect
中并传递 someUIState
作为键,这样每当它改变时块就会被重新触发.
val someUIState by viewModel.someUIState.collectAsState()
LaunchedEffect(someUiState) {
when(someUiState) {
// Same as in the question
}
}
或者,您可以只在 LaunchedEffect
.
中收集流量
LaunchedEffect(Unit) {
viewModel.someUIState.collect { uiState ->
when(uiState) {
// Same as in the question
}
}
}
虽然您可以使用 Arpit 提供的解决方案,但我个人更喜欢在视图模型中管理 API 调用的状态。很容易滥用 LaunchEffect。此外,LaunchEffect - 在我看来 - 确实应该是 UI 相关的东西,而不是用于处理对某些后端的 API 调用。由于您已经有一个用于处理状态的变量 - someUIState
- 仅当状态设置为 Nothing
时才进行 API 调用。像这样:
class FancyViewModel() : ViewModel() {
private val _someUIState = MutableStateFlow<FancyWrapper>(FancyWrapper.Nothing)
val someUIState: StateFlow<FancyWrapper> = _someUIState
fun attemptAPICall() = viewModelScope.launch {
if (_someUIState.value != FancyWrapper.Nothing) {
return
}
_someUIState.value = FancyWrapper.Loading
when (val res = doAPICall()) {
is APIWrapper.Success -> _someUIState.value = FancyWrapper.Loading(res.vaue.data)
is APIWrapper.Error -> _someUIState.value = FancyWrapper.Error("Error!")
}
}
}
我对小吃店做的其他解决方案是通知视图模型数据已被消耗:
在你的 FancyUI 中:
...
when(val res = someUIState.value){
is FancyWrapper.Loading-> showProgress = true
is FancyWrapper.Success-> {
...
viewModel.onResultConsumed()
}
is FancyWrapper.Error-> showProgress = false
}
...
并且在您的视图模型中:
class FancyViewModel() : ViewModel() {
private val _someUIState = MutableStateFlow<FancyWrapper>(FancyWrapper.Nothing)
...
fun onResultConsumed() {
_someUIState.tryEmit(FancyWrapper.Nothing)
}
}
编辑
如果有人还在寻找这个,这里有另一个解决方案:
创建事件class:
/*
* Copyright 2017, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
事件class最初是为LiveData创建的,但在Flow上工作得很好,如果已经被消费,事件的价值将是bull,这样你可以保存对视图模型的调用。
在您的屏幕中使用它:
...
when(val res = someUIState.value){
is FancyWrapper.Loading-> showProgress = true
is FancyWrapper.Success-> {
res.event.getContentIfNotHandled?.let {
//do stuff here
...
}
}
is FancyWrapper.Error-> showProgress = false
}
...
要在视图模型中使用,您只需为要显示的状态创建一个事件,例如:
_someUIState.tryEmit(FancyWrapper.Success(event = Event(data)))
我正在 ViewModel 中执行 API 调用并在可组合项中观察它,如下所示:
class FancyViewModel(): ViewModel(){
private val _someUIState =
MutableStateFlow<FancyWrapper>(FancyWrapper.Nothing)
val someUIState: StateFlow<FancyWrapper> =
_someUIState
fun attemptAPICall() = viewModelScope.launch {
_someUIState.value = FancyWrapper.Loading
when(val res = doAPICall()){
is APIWrapper.Success -> _someUIState.value = FancyWrapper.Loading(res.vaue.data)
is APIWrapper.Error -> _someUIState.value = FancyWrapper.Error("Error!")
}
}
}
在组合中,我正在听 'someUIState' 这样的:
@Composable
fun FancyUI(viewModel: FancyViewModel){
val showProgress by remember {
mutableStateOf(false)
}
val openDialog = remember { mutableStateOf(false) }
val someUIState =
viewModel.someUIState.collectAsState()
when(val res = someUIState.value){
is FancyWrapper.Loading-> showProgress = true
is FancyWrapper.Success-> {
showProgress = false
if(res.value.error)
openDialog.value = true
else
navController.navigate(Screen.OtherScreen.route)
}
is FancyWrapper.Error-> showProgress = false
}
if (openDialog.value){
AlertDialog(
..
)
}
Scaffold(
topBar = {
Button(onClick={viewModel.attemptAPICall()}){
if(showProgress)
CircularProgressIndicator()
else
Text("Click")
}
}
){
SomeUI()
}
}
我面临的问题是 FancyUI 可组合项中某些 UIState 的 'when' 块代码在可组合项重组期间被多次触发,即使没有单击脚手架中的按钮(例如:当 AlertDialog 出现时)。我哪里做错了?在 Composable 中使用 StateFlow 观察数据的正确更好方法是什么?
如果你只想处理每个 someUIState
值一次,你应该把它放在一个 LaunchedEffect
中并传递 someUIState
作为键,这样每当它改变时块就会被重新触发.
val someUIState by viewModel.someUIState.collectAsState()
LaunchedEffect(someUiState) {
when(someUiState) {
// Same as in the question
}
}
或者,您可以只在 LaunchedEffect
.
LaunchedEffect(Unit) {
viewModel.someUIState.collect { uiState ->
when(uiState) {
// Same as in the question
}
}
}
虽然您可以使用 Arpit 提供的解决方案,但我个人更喜欢在视图模型中管理 API 调用的状态。很容易滥用 LaunchEffect。此外,LaunchEffect - 在我看来 - 确实应该是 UI 相关的东西,而不是用于处理对某些后端的 API 调用。由于您已经有一个用于处理状态的变量 - someUIState
- 仅当状态设置为 Nothing
时才进行 API 调用。像这样:
class FancyViewModel() : ViewModel() {
private val _someUIState = MutableStateFlow<FancyWrapper>(FancyWrapper.Nothing)
val someUIState: StateFlow<FancyWrapper> = _someUIState
fun attemptAPICall() = viewModelScope.launch {
if (_someUIState.value != FancyWrapper.Nothing) {
return
}
_someUIState.value = FancyWrapper.Loading
when (val res = doAPICall()) {
is APIWrapper.Success -> _someUIState.value = FancyWrapper.Loading(res.vaue.data)
is APIWrapper.Error -> _someUIState.value = FancyWrapper.Error("Error!")
}
}
}
我对小吃店做的其他解决方案是通知视图模型数据已被消耗: 在你的 FancyUI 中:
...
when(val res = someUIState.value){
is FancyWrapper.Loading-> showProgress = true
is FancyWrapper.Success-> {
...
viewModel.onResultConsumed()
}
is FancyWrapper.Error-> showProgress = false
}
...
并且在您的视图模型中:
class FancyViewModel() : ViewModel() {
private val _someUIState = MutableStateFlow<FancyWrapper>(FancyWrapper.Nothing)
...
fun onResultConsumed() {
_someUIState.tryEmit(FancyWrapper.Nothing)
}
}
编辑
如果有人还在寻找这个,这里有另一个解决方案:
创建事件class:
/*
* Copyright 2017, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
事件class最初是为LiveData创建的,但在Flow上工作得很好,如果已经被消费,事件的价值将是bull,这样你可以保存对视图模型的调用。
在您的屏幕中使用它:
...
when(val res = someUIState.value){
is FancyWrapper.Loading-> showProgress = true
is FancyWrapper.Success-> {
res.event.getContentIfNotHandled?.let {
//do stuff here
...
}
}
is FancyWrapper.Error-> showProgress = false
}
...
要在视图模型中使用,您只需为要显示的状态创建一个事件,例如:
_someUIState.tryEmit(FancyWrapper.Success(event = Event(data)))