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 one rror(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!
CodePudding user response:
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..")
}
}
}
}
}
...
}