2

I develop a small-scaled jetpack compose project. I faced an issue with the click of the button.

Furthermore, I've used some base class/function designs for this project. Project uses BaseViewModel class and BaseComposableScreen composable function to generalize basic communication of view and view-model.

Here is the base things:

@Composable
fun <State, Event> BaseComposableScreen(
    navController: NavController,
    viewModel: BaseViewModel<State, Event>,
    content: @Composable (coroutineScope: CoroutineScope) -> Unit,
) {
    val coroutineScope = rememberCoroutineScope()

    LaunchedEffect(Unit) {
        viewModel.effect.collect { effect ->
            when (effect) {
                is BasicEffect.NavigateToEffect -> {
                    coroutineScope.launch {
                        navController.navigate(effect.route)
                    }
                }
                is BasicEffect.NavigateBackToEffect -> {
                    coroutineScope.launch {
                        navController.popBackStack(effect.destination, effect.inclusive)
                    }
                }
                is BasicEffect.NavigateBackEffect -> {
                    coroutineScope.launch {
                        navController.popBackStack()
                    }
                }
            }
        }
    }

    content(coroutineScope)
}
abstract class BaseViewModel<State, Event> : ViewModel() {

    private val mutex = Mutex()
    private val exceptionHandler = CoroutineExceptionHandler(::onError)

    abstract fun provideInitialState(): State

    private val _state = MutableStateFlow(provideInitialState())
    val state: StateFlow<State> = _state.asStateFlow()

    private val _effect = Channel<BaseEffect>(Channel.BUFFERED)
    val effect: Flow<BaseEffect> = _effect.receiveAsFlow()

    //optional override
    open fun onEvent(event: Event) {}

    open fun onError(context: CoroutineContext, throwable: Throwable) {

    }

    protected fun emitState(state: State) {
        launchOnMain {
            mutex.withLock {
                _state.emit(state)
            }
        }
    }

    protected fun emitEffect(effect: BaseEffect) {
        launchOnMain {
            _effect.send(effect)
        }
    }

    protected fun <P, R, U : BaseUseCase<P, R>> executeUseCase(
        useCase: U,
        param: P,
        onComplete: ((R) -> Unit)? = null,
    ) {
        launchOnMain {
            val result = useCase.start(param)
            onComplete?.invoke(result)
        }
    }

    protected fun launchOnMain(block: suspend CoroutineScope.() -> Unit): Job {
        return viewModelScope.launch(exceptionHandler, block = block)
    }

    protected fun launchOnIO(block: suspend CoroutineScope.() -> Unit): Job {
        return viewModelScope.launch(exceptionHandler + Dispatchers.IO, block = block)
    }

    protected fun launchOnDefault(block: suspend CoroutineScope.() -> Unit): Job {
        return viewModelScope.launch(exceptionHandler + Dispatchers.Default, block = block)
    }

    protected fun <T> Flow<T>.launchFlow(scope: CoroutineScope = viewModelScope): Job =
        this.catch {
            exceptionHandler.handleException(currentCoroutineContext(), it)
        }.launchIn(scope)

}
abstract class BaseEffect

sealed class BasicEffect: BaseEffect() {

    data class NavigateToEffect(val route: String) : BaseEffect()

    data class NavigateBackToEffect(
        val destination: String,
        val inclusive: Boolean = false,
    ) : BaseEffect()

    object NavigateBackEffect : BaseEffect()

}

I've implemented these base structures for a composable screen and a view model of it. Here they are:

class ChurchViewModel : BaseViewModel<Unit, ChurchEvent>() {

    override fun provideInitialState() = Unit

    override fun onEvent(event: ChurchEvent) {
        when (event) {
            is ChurchEvent.PrayToGod -> {
                emitEffect(ChurchEffect.GodListen)
            }
        }
    }

}

sealed class ChurchEffect : BaseEffect() {
    object GodListen : ChurchEffect()
}

sealed class ChurchEvent {
    object PrayToGod : ChurchEvent()
}
@Composable
fun ChurchScreen(navController: NavController) {
    val viewModel = viewModel<ChurchViewModel>()
    val scaffoldState = rememberScaffoldState()

    LaunchedEffect(Unit) {
        viewModel.effect.collect { effect ->
            when (effect) {
                ChurchEffect.GodListen -> {
                    scaffoldState.snackbarHostState.showSnackbar("God listens..")
                }
            }
        }
    }

    BaseComposableScreen(navController = navController, viewModel = viewModel) {
        ChurchScreenContent(
            scaffoldState = scaffoldState,
            onPrayToGod = {
                viewModel.onEvent(ChurchEvent.PrayToGod)
            }
        )
    }
}

@Composable
private fun ChurchScreenContent(
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    onPrayToGod: () -> Unit = { },
) {
    Scaffold(scaffoldState = scaffoldState) { paddingValues ->
        Column(
            modifier = Modifier
                .padding(paddingValues)
                .fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.SpaceEvenly,
        ) {
            Text(text = "Church")
            Icon(
                painter = painterResource(id = R.drawable.ic_baseline_location_city_24),
                contentDescription = "Church image",
                modifier = Modifier.size(90.dp),
            )
            Button(onClick = onPrayToGod) {
                Text(text = "Pray to God")
            }
        }
    }
}

Problem is that when I click the "Pray to God" button. The code that it calls works only odd times. For example, first click works, second time is not. Third click works, forth one not.

I don't know the reason exactly, please, help me to clarify this situation!

1 Answer 1

4

Your coroutine started by LaunchedEffect is used to listen for the effects and also showing the snackbar. I assume these operations can block each other, so I recommend you to use the separate coroutine scope to show the snackbar.

@Composable
fun ChurchScreen(navController: NavController) {
    val viewModel = viewModel<ChurchViewModel>()
    val scaffoldState = rememberScaffoldState()
    val coroutineScope = rememberCoroutineScope() // Here is your scope for showing snackbar

    LaunchedEffect(Unit) {
        viewModel.effect.collect { effect ->
            when (effect) {
                ChurchEffect.GodListen -> {
                    coroutineScope.launch { 
                        scaffoldState.snackbarHostState.showSnackbar("God listens..") 
                    }
                }
            }
        }
    }

    ...
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.