Jetpack Compose 导航和 StateFlow 的问题
Problem with Jetpack Compose Navigation and StateFlow
Composable
函数 Application
创建了一个 NavHostController
,它定义了 2 个目标。一个 StartScreen
和一个 ContentScreen
。 StartScreen
只有一个按钮,它会触发模拟后端请求并根据请求的状态更改状态(使用 kotlins StateFlow
)。当结果返回时,NavController
s navigate
方法被调用以在 ContentScreen
.
上显示返回的内容
问题:
状态 Init
和 Loading
正常工作,但是一旦应该显示内容,ContentScreen
就会循环重绘并且不会停止。
我做错了什么?
/** Dependencies
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation "androidx.compose.ui:ui:1.0.2"
implementation "androidx.compose.ui:ui:1.0.2"
implementation "androidx.compose.material:material:1.0.2"
implementation "androidx.compose.ui:ui-tooling-preview:1.0.2"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation "androidx.navigation:navigation-compose:2.4.0-alpha08"
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07'
implementation 'androidx.activity:activity-compose:1.3.1'
*
* **/
sealed class MainState {
object Init : MainState()
object Loading : MainState()
data class Content(val data: String) : MainState()
}
class MainViewModel : ViewModel() {
private val _state = MutableStateFlow<MainState>(MainState.Init)
val state: StateFlow<MainState> = _state
fun dosomething() {
viewModelScope.launch {
_state.value = MainState.Loading
// emulating some BE call
delay(4000)
_state.value = MainState.Content("some backend result")
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Application()
}
}
}
@Composable
fun Application() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "start") {
composable("start") {
val viewmodel: MainViewModel = viewModel()
val state by viewmodel.state.collectAsState()
when (val state = state) {
is MainState.Content -> navController.navigate("content/${state.data}")
is MainState.Loading -> LoadingScreen()
MainState.Init -> StartScreen()
}
}
composable(
"content/{content}",
arguments = listOf(
navArgument("content") {
type = NavType.StringType
}
)
) {
ContentScreen(content = it.arguments!!.getString("content")!!)
}
}
}
@Composable
fun LoadingScreen() {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
CircularProgressIndicator()
}
}
@Composable
fun StartScreen(viewmodel: MainViewModel = viewModel()) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Button(onClick = { viewmodel.dosomething() }) {
Text(text = "Click Me!")
}
}
}
@Composable
fun ContentScreen(content: String) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Card(modifier = Modifier.padding(8.dp)) {
Text(text = content)
}
}
}
在 compose 中,您正在使用视图构建器创建 UI。这个函数可以多次调用,当你开始使用动画时,它甚至可以在每一帧上重新组合。
这就是为什么您不应该直接从可组合函数执行任何副作用。您需要使用 side effects
在这种情况下应该使用LaunchedEffect(Unit)
:里面的代码只会启动一次。
when (val state = state) {
is MainState.Content -> LaunchedEffect(Unit) {
navController.navigate("content/${state.data}")
}
is MainState.Loading -> LoadingScreen()
MainState.Init -> StartScreen()
}
Composable
函数 Application
创建了一个 NavHostController
,它定义了 2 个目标。一个 StartScreen
和一个 ContentScreen
。 StartScreen
只有一个按钮,它会触发模拟后端请求并根据请求的状态更改状态(使用 kotlins StateFlow
)。当结果返回时,NavController
s navigate
方法被调用以在 ContentScreen
.
问题:
状态 Init
和 Loading
正常工作,但是一旦应该显示内容,ContentScreen
就会循环重绘并且不会停止。
我做错了什么?
/** Dependencies
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation "androidx.compose.ui:ui:1.0.2"
implementation "androidx.compose.ui:ui:1.0.2"
implementation "androidx.compose.material:material:1.0.2"
implementation "androidx.compose.ui:ui-tooling-preview:1.0.2"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation "androidx.navigation:navigation-compose:2.4.0-alpha08"
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07'
implementation 'androidx.activity:activity-compose:1.3.1'
*
* **/
sealed class MainState {
object Init : MainState()
object Loading : MainState()
data class Content(val data: String) : MainState()
}
class MainViewModel : ViewModel() {
private val _state = MutableStateFlow<MainState>(MainState.Init)
val state: StateFlow<MainState> = _state
fun dosomething() {
viewModelScope.launch {
_state.value = MainState.Loading
// emulating some BE call
delay(4000)
_state.value = MainState.Content("some backend result")
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Application()
}
}
}
@Composable
fun Application() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "start") {
composable("start") {
val viewmodel: MainViewModel = viewModel()
val state by viewmodel.state.collectAsState()
when (val state = state) {
is MainState.Content -> navController.navigate("content/${state.data}")
is MainState.Loading -> LoadingScreen()
MainState.Init -> StartScreen()
}
}
composable(
"content/{content}",
arguments = listOf(
navArgument("content") {
type = NavType.StringType
}
)
) {
ContentScreen(content = it.arguments!!.getString("content")!!)
}
}
}
@Composable
fun LoadingScreen() {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
CircularProgressIndicator()
}
}
@Composable
fun StartScreen(viewmodel: MainViewModel = viewModel()) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Button(onClick = { viewmodel.dosomething() }) {
Text(text = "Click Me!")
}
}
}
@Composable
fun ContentScreen(content: String) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Card(modifier = Modifier.padding(8.dp)) {
Text(text = content)
}
}
}
在 compose 中,您正在使用视图构建器创建 UI。这个函数可以多次调用,当你开始使用动画时,它甚至可以在每一帧上重新组合。
这就是为什么您不应该直接从可组合函数执行任何副作用。您需要使用 side effects
在这种情况下应该使用LaunchedEffect(Unit)
:里面的代码只会启动一次。
when (val state = state) {
is MainState.Content -> LaunchedEffect(Unit) {
navController.navigate("content/${state.data}")
}
is MainState.Loading -> LoadingScreen()
MainState.Init -> StartScreen()
}