I'm creating a project with Compose
, but I ran into a situation that I couldn't solve.
View Model:
data class OneState(
val name: String = "",
val city: String = ""
)
sealed class OneChannel {
object FirstStepToSecondStep : OneChannel()
object Finish : OneChannel()
}
@HiltViewModel
class OneViewModel @Inject constructor() : ViewModel() {
private val viewModelState = MutableStateFlow(OneState())
val screenState = viewModelState.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = viewModelState.value
)
private val _channel = Channel<OneChannel>()
val channel = _channel.receiveAsFlow()
fun changeName(value: String) {
viewModelState.update { it.copy(name = value) }
}
fun changeCity(value: String) {
viewModelState.update { it.copy(city = value) }
}
fun firstStepToSecondStep() {
Log.d("OneViewModel", "start of method first step to second step")
if (viewModelState.value.name.isBlank()) {
Log.d("OneViewModel", "name is empty, nothing should be done")
return
}
Log.d(
"OneViewModel",
"name is not empty, first step to second step event will be send for composable"
)
viewModelScope.launch {
_channel.send(OneChannel.FirstStepToSecondStep)
}
}
fun finish() {
Log.d("OneViewModel", "start of method finish")
if (viewModelState.value.city.isBlank()) {
Log.d("OneViewModel", "city is empty, nothing should be done")
return
}
Log.d(
"OneViewModel",
"city is not empty, finish event will be send for composable"
)
viewModelScope.launch {
_channel.send(OneChannel.Finish)
}
}
}
This ViewModel
has a MutableStateFlow
, a StateFlow
to be collected on composable screens
and a Channel
/Flow
for "one time events".
The first two methods are to change a respective state and the last two methods are to validate some logic and then send an event through the Channel
.
Composables:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FirstStep(
viewModel: OneViewModel,
nextStep: () -> Unit
) {
val state by viewModel.screenState.collectAsState()
LaunchedEffect(key1 = Unit) {
Log.d("FirstStep (Composable)", "start of launched effect block")
viewModel.channel.collect { channel ->
when (channel) {
OneChannel.FirstStepToSecondStep -> {
Log.d("FirstStep (Composable)", "first step to second step action")
nextStep()
}
else -> Log.d(
"FirstStep (Composable)",
"another action that should be ignored in this scope"
)
}
}
}
Column(modifier = Modifier.fillMaxSize()) {
TextField(
modifier = Modifier
.fillMaxWidth()
.padding(all = 16.dp),
value = state.name,
onValueChange = { viewModel.changeName(value = it) },
placeholder = { Text(text = "Type our name") }
)
Spacer(modifier = Modifier.weight(weight = 1F))
Button(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
onClick = { viewModel.firstStepToSecondStep() }
) {
Text(text = "Next Step")
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SecondStep(
viewModel: OneViewModel,
prevStep: () -> Unit,
finish: () -> Unit
) {
val state by viewModel.screenState.collectAsState()
LaunchedEffect(key1 = Unit) {
Log.d("SecondStep (Composable)", "start of launched effect block")
viewModel.channel.collect { channel ->
when (channel) {
OneChannel.Finish -> {
Log.d("SecondStep (Composable)", "finish action //todo")
finish()
}
else -> Log.d(
"SecondStep (Composable)",
"another action that should be ignored in this scope"
)
}
}
}
Column(modifier = Modifier.fillMaxSize()) {
TextField(
modifier = Modifier
.fillMaxWidth()
.padding(all = 16.dp),
value = state.city,
onValueChange = { viewModel.changeCity(value = it) },
placeholder = { Text(text = "Type our city name") }
)
Spacer(modifier = Modifier.weight(weight = 1F))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
Button(
modifier = Modifier.weight(weight = 1F),
onClick = prevStep
) {
Text(text = "Previous Step")
}
Button(
modifier = Modifier.weight(weight = 1F),
onClick = { viewModel.finish() }
) {
Text(text = "Finish")
}
}
}
}
@OptIn(ExperimentalPagerApi::class)
@Composable
fun OneScreen(viewModel: OneViewModel = hiltViewModel()) {
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState(initialPage = 0)
val pages = listOf<@Composable () -> Unit>(
{
FirstStep(
viewModel = viewModel,
nextStep = {
coroutineScope.launch {
pagerState.animateScrollToPage(page = pagerState.currentPage 1)
}
}
)
},
{
SecondStep(
viewModel = viewModel,
prevStep = {
coroutineScope.launch {
pagerState.animateScrollToPage(page = pagerState.currentPage - 1)
}
},
finish = {}
)
}
)
Column(modifier = Modifier.fillMaxSize()) {
HorizontalPager(
modifier = Modifier
.fillMaxWidth()
.weight(weight = 1F),
state = pagerState,
count = pages.size,
userScrollEnabled = false
) { index ->
pages[index]()
}
HorizontalPagerIndicator(
modifier = Modifier
.padding(vertical = 16.dp)
.align(alignment = Alignment.CenterHorizontally),
pagerState = pagerState,
activeColor = MaterialTheme.colorScheme.primary
)
}
}
OneScreen
has a HorizontalPager
(from the Accompanist library) which receives two other composables, FirstStep
and SecondStep
, these two composables have their own LaunchedEffect
to collect any possible event coming from the View Model.
Dependencies used:
implementation 'androidx.navigation:navigation-compose:2.5.2'
implementation 'com.google.dagger:hilt-android:2.43.2'
kapt 'com.google.dagger:hilt-android-compiler:2.43.2'
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
implementation 'com.google.accompanist:accompanist-pager:0.25.1'
implementation 'com.google.accompanist:accompanist-pager-indicators:0.25.1'
The problem:
After typing something in the name field and clicking to go to the next step, the flow happens normally. When clicking to go back to the previous step, it also works normally. But now when clicking to go to the next step again, the collect
in the LaunchedEffect
of the FirstStep
is not collected, instead the collect
in LaunchedEffect
of the SecondStep
is, resulting in no action, and if click again, then collect
in FirstStep
works.
Some images that follow the logcat: