1

Hey I am working in MutableStateFlow in jetpack compose. I am trying to load spinner in next screen but it's not working. I am started learning jetpack compose. I didn't find the proper way in jetpack compose. I know MutableStateFlow in legacy way. I'll try explain what I am trying to do and what I want to achieve. I have a Button in the composable function, when I press the Button I am going into the viewModel and calling the api inside viewModelScope.launch. On that basis I have create a class called ResultFetchState which is sealed class, inside that I used UI state. When my api get any thing it will route to success, error and loading. The main problem is when I am using collectAsState() the data is going onSuccess but onLoading is not working. Hope you understand this.

MainActivityViewModel.kt

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

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

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

I am not adding my whole activity code. I am adding my composable function. If you need to see my whole activity click on class name below.

MainActivity.kt

@OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun SetupView(viewModel: MainActivityViewModel = koinViewModel()) {
        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 -> {
                logE("data >> ${state.sportResultResponse}")
            }
            is ResultFetchState.IsLoading -> {
                if (state.isLoading) {
                    logE("data loading >")
                    ResultScreen()
                }
            }
            is ResultFetchState.OnError -> {
                logE("data >> ${state.response}")
            }
            is ResultFetchState.OnEmpty -> {}
        }
    }

I though there is problem in my lifecycle. So I search in stack overflow of LaunchedEffect but still not working. Another question is there any use of LaunchedEffect in my code I am little bit confused on this side effect. My github project link. Thanks

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

1 Answers1

1

You are setting value of stateResultFetchState consecutively

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

You can set to loading before getting sport results and in your ui you check if loading is true you need to set it with true

fun getSportResult() {
    viewModelScope.launch {
        stateResultFetchState.value = ResultFetchState.IsLoading(true)
        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)
            }
        )
    }
}

Also there is an issue with your UI either. Even if you correctly get loading state you might be able to see them because Scaffold places children in content Composable on top of each other with

bodyContentPlaceables.forEach {
            it.place(0, 0)
        }

You might need to use Composable that covers your content area as root Comopsable.

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • Where should I stop ui loading then? – Kotlin Learner Jul 20 '22 at 06:11
  • In compose you change states. When you change your state from loading to error or success it will be removed from composition. and conditional block that is satisfied will enter composition – Thracian Jul 20 '22 at 06:15
  • You can check this answer about conditional recomposition. https://stackoverflow.com/questions/72705876/remember-in-composition-is-forgotten/72709495#72709495 – Thracian Jul 20 '22 at 06:15
  • Thats great.. Can you please also explain me what is the LaunchedEffect use ? Is there necessary in my code. I am asking because in this [SO](https://stackoverflow.com/a/69419545/11560810) is using which is similar to my case I think so. – Kotlin Learner Jul 20 '22 at 06:40
  • LaunchedEffect is a remember under the hood launches your code on first run or composition, then if they key or you keys you set change same as remember. block invoked based on keys. In that question you see `when(val r = viewModel.s.value)` is checked. This part runs on every time `LazyColumn` is recomposed checks state and calls correct conditional composition. But when it's already in LoadingState if it recomposed again there won't be recompositions for the Loading Composable. LaunchedEffect seems redundant in that answer. – Thracian Jul 20 '22 at 06:52
  • if (showProgressBarState.value) { ShowProgressBar() } is before LazyColumn in that example that's why it doesn't show if LazyColumn is displayed. Box(contentAlignment = Alignment.Center){LazyColumn() when(...){Loading} is how you show a loading over your Composable based on your ui state – Thracian Jul 20 '22 at 06:52
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/246595/discussion-between-thracian-and-vivek-modi). – Thracian Jul 20 '22 at 07:01