4

The Composable function Application creates a NavHostController which defines 2 targets. A StartScreen and a ContentScreen. The StartScreen has just a single button, which triggers a simulated backend request and changes State (using kotlins StateFlow) depending on the state of the request. When the result comes back, the NavControllers navigate method gets called in order to show the returned content on the ContentScreen.

The Problem: The states Init and Loading work properly, but as soon as the content is supposed to be shown, the ContentScreen gets redrawn in a loop and doesn't stop.

What do I do wrong here?


/** 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)
        }
    }
}

andre
  • 1,618
  • 2
  • 19
  • 38

1 Answers1

7

In compose you're creating UI with view builders. This function can be called many times, when you start using animations it even can be recomposed on each frame.

That's why you shouldn't perform any side effects directly from composable function. You need to use side effects

In this case LaunchedEffect(Unit) should be used: code inside will only be launched once.

when (val state = state) {
    is MainState.Content -> LaunchedEffect(Unit) {
        navController.navigate("content/${state.data}")
    }
    is MainState.Loading -> LoadingScreen()
    MainState.Init -> StartScreen()
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220