2

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
                    }
                },
                onError = {
                    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

UPDATE

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SetupMainActivityView(
    viewModel: MainActivityViewModel = koinViewModel(),
    navigateToNext: (nearestResult: ArrayList<NearestResult>) -> Unit,
) {
    Scaffold(topBar = {
        TopAppBar(
            title = { Text(text = stringResource(id = R.string.app_name)) },
            backgroundColor = getBackgroundColor(),
            elevation = 0.dp
        )
    }, content = { padding ->
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(padding)
                .background(getBackgroundColor()),
            contentAlignment = Center
        ) {
            when (val state = viewModel.stateResultFetchState.collectAsState().value) {
                is ResultFetchState.OnSuccess -> {
                    LaunchedEffect(Unit) {
                        navigateToNext(state.nearestResult)
                    }
                }
                is ResultFetchState.IsLoading -> {
                    LoadingFunction()
                }
                is ResultFetchState.OnError,
                is ResultFetchState.OnEmpty -> {
                    ActivityContent(viewModel)
                }
            }
        }
    })
}

After doing this my ResultScreen calling twice. Is it normal?

Kotlin Learner
  • 3,995
  • 6
  • 47
  • 127

1 Answers1

2

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.

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • thanks for replying to me. Can you please guide me why do you replace `column` with `Box` ? – Kotlin Learner Jul 21 '22 at 07:51
  • @vivekmodi your column should be instead of `YourContent`, `Box` is just a container to apply some modifiers used for all children – Phil Dukhov Jul 21 '22 at 08:29
  • Great thank a million. You are great. I really appreciate it. My code is working fine. – Kotlin Learner Jul 21 '22 at 08:53
  • Hey @PhilDukhov my [ResultScreen](https://github.com/vivek-modi/SportsResult/blob/master/app/src/main/java/com/vivek/sportsresult/ui/screen/ResultScreen.kt) is calling twice when I click on button inside this [MainActivity](https://github.com/vivek-modi/SportsResult/blob/master/app/src/main/java/com/vivek/sportsresult/ui/screen/MainActivity.kt). Is it normal ? – Kotlin Learner Jul 21 '22 at 19:13
  • @vivekmodi sure, read first part of the [linked answer](https://stackoverflow.com/questions/71159644/composable-is-recomposing-endlessly-after-flow-collect/71159855#71159855) which I've mentioned in this answer – Phil Dukhov Jul 22 '22 at 05:22
  • I think i got it now. Thank you so much. I have another issue regarding back navigation. Can you please look into this [issue](https://stackoverflow.com/q/73072904/11560810) – Kotlin Learner Jul 22 '22 at 07:24