Home > Blockchain >  Ui state not working correctly in jetpack compose
Ui state not working correctly in jetpack compose

Time:07-21

I am using MutableStateFlow UI State in jetpack compose. I am not getting proper flow of my UI event. In UI state there is Empty, Loading, Success and Error state. I setup a Empty state when I initialise a variable. When I am starting to call api before that I am trigger Loading state. On that basis I am triggering Success or Error event.

Note: I am not adding imports and package name. If you want to see full code please click a name of class you will redirect to my repository.

MainActivityViewModel.kt

class MainActivityViewModel(private val resultRepository: ResultRepository) : ViewModel() {

    val stateResultFetchState = MutableStateFlow<ResultFetchState>(ResultFetchState.OnEmpty)

    fun getSportResult() {
        viewModelScope.launch {
            stateResultFetchState.value = ResultFetchState.IsLoading
            val result = resultRepository.getSportResult()
            delay(5000)
            result.handleResult(
                onSuccess = { response ->
                    if (response != null) {
                        stateResultFetchState.value = ResultFetchState.OnSuccess(response)
                    } else {
                        stateResultFetchState.value = ResultFetchState.OnEmpty
                    }
                },
                one rror = {
                    stateResultFetchState.value =
                        ResultFetchState.OnError(it.errorResponse?.errorMessage)
                }
            )
        }
    }
}

MainActivity.kt

internal lateinit var networkConnection: NetworkConnection

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        networkConnection = NetworkConnection(application)
        setContent {
            SportsResultTheme {
                SetupConnectionView()
            }
        }
    }
}

@Composable
fun SetupConnectionView() {
    val isConnected = networkConnection.observeAsState()
    if (isConnected.value == true) {
        NavigationGraph()
    } else {
        NoInternetView()
    }
}

@Composable
fun NoInternetView() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(getBackgroundColor()),
        contentAlignment = Center,

        ) {
        val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.nointernet))
        LottieAnimation(
            composition,
            iterations = LottieConstants.IterateForever
        )
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SetupMainActivityView(
    viewModel: MainActivityViewModel = koinViewModel(),
    navigateToNext: (state: String) -> Unit,
) {
    Scaffold(topBar = {
        TopAppBar(
            title = { Text(text = stringResource(id = R.string.app_name)) },
            backgroundColor = getBackgroundColor(),
            elevation = 0.dp
        )
    }, content = { padding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .background(getBackgroundColor())
                .padding(padding),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Button(onClick = {
                viewModel.getSportResult()
            }) {
                Text(text = stringResource(id = R.string.get_result))
            }
        }
    })
    when (val state = viewModel.stateResultFetchState.collectAsState().value) {
        is ResultFetchState.OnSuccess -> {
            navigateToNext("loading $state")
        }
        is ResultFetchState.IsLoading -> {
            LoadingFunction()
        }
        is ResultFetchState.OnError -> {}
        is ResultFetchState.OnEmpty -> {}
    }
}


@Composable
fun LoadingFunction() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(getBackgroundColor()),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        CircularProgressIndicator()
    }
}

I am adding my navigation graph so you will clearly see what I am trying to do.

NavigationGraph.kt

@Composable
internal fun NavigationGraph() {
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = ScreenRoute.Home.route) {
        composable(ScreenRoute.Home.route) {
            SetupMainActivityView { state ->
                navController.navigate(ScreenRoute.Result.route   "/{$state}")
            }
        }

        composable(
            ScreenRoute.Result.route   "/{state}",
            arguments = listOf(
                navArgument("state") { type = NavType.StringType }
            )
        ) { backStackEntry ->
            ResultScreen(backStackEntry.arguments?.getString("state").orEmpty())
        }
    }
}

ResultScreen.kt

@Composable
fun ResultScreen(state: String) {
    Log.e("TAG", "ResultScreen: $state" )

}

Actual Output

when you click on Button it started Loading screen. After Loading screen my Button screen appears than my Result Screen appears. You can see in my video.

Button Screen -> Loading Screen -> Again Button Screen -> Result Screen.

Expected Output

Button Screen -> Loading Screen -> Result Screen.

My Github project link. Can you guys guide me what I am doing wrong here. Many Thanks

CodePudding user response:

During loading you overlap the button view with the loading view, but when you succeed you remove the loading view, so the button view appears for the transition.

Depending on the expected behavior, you can move your when inside the content, and display content only on empty/error - it might make sense to leave the option to click back to cancel the request.

content = { padding ->
    Box(Modifier.fillMaxSize().padding(padding).background(getBackgroundColor())) {
        when (val state = viewModel.stateResultFetchState.collectAsState().value) {
            is ResultFetchState.OnSuccess -> {
                LaunchedEffect(Unit){
                    navigateToNext("loading $state")
                }
            }
            is ResultFetchState.IsLoading -> {
                LoadingFunction()
            }
            is ResultFetchState.OnError, is ResultFetchState.OnEmpty -> {
                YourContent()
            }
        }
    }
})

Or add LoadingFunction() inside ResultFetchState.OnSuccess, so that this view doesn't disappear from the screen during the transition.

is ResultFetchState.OnSuccess -> {
    LaunchedEffect(Unit){
        navigateToNext("loading $state")
    }
    LoadingFunction()
}

Also see this answer for why calling navigateToNext as you do is unsafe and why I've added LaunchedEffect.

  • Related