Reputation: 3190
I have screen that should display one three things: (I'm also very new to Jetpack Compose)
A Loader() if loading An AlertDialog() if there's an error And finally, based on an int from my viewModel, a new ScreenX()
I make an API call in my viewModel and update the 3 state variables accordingly. The problem is that all of the views seem to be appearing over top of each other.
If I try and return Loader()
, I get an error.
Maybe this isn't the correct way to do this, but is there any way you can have dynamic screens in a Composable?
@Composable
fun ConnectionScreen(
ip: String,
viewModel: BridgeViewModel,
navController: NavHostController
) {
val page = viewModel.page.collectAsState()
val loading = viewModel.loading.collectAsState()
val error = viewModel.error.collectAsState()
Scaffold {
if (loading.value) {
Loader()
}
error.value?.let {
AlertDialogWithButtons(
title = "Error",
message = it,
onNegativeClick = { },
onPositiveClick = {},
)
}
when (page.value) {
0 -> Screen0()
1 -> Screen1()
2 -> Screen2()
}
}
}
Upvotes: 2
Views: 2959
Reputation: 3190
I decided to create an encapsulating data class to better represent UI state. It's not the best, and unfortunately not reusable (I'll need to create a new data class for every screen? Even though most will be using an API, so Loading and Error should be reusable).
enum class ResourceState { LOADING, ERROR, LINK, FINISH }
data class CreateResourceState(
val loading: Boolean = false,
val error: String? = null,
val page: Int = 0,
) {
val state: ResourceState
get() {
return if (loading) ResourceState.LOADING
else if (error != null) ResourceState.ERROR
else if (page == 1) ResourceState.LINK
else if (page == 2) ResourceState.FINISH
else ResourceState.LOADING
}
}
Then in my ViewModel
private val _uiState = MutableStateFlow(CreateResourceState(loading = true))
val uiState: StateFlow<CreateResourceState> = _uiState
...
_uiState.value = CreateResourceState(error = exception.message)
...
_uiState.value = CreateResourceState(page = 1)
Finally in the UI
val uiState = viewModel.uiState.collectAsState()
when (uiState.value.state) {
ResourceState.LOADING -> Loader()
ResourceState.ERROR -> ErrorDialog()
ResourceState.LINK -> Screen1()
ResourceState.FINISH -> Screen2()
}
It still seems weird to me that you can only return one and only one Composable. Maybe I'm too used to Flutter, where you can dynamically return any Widget at any time inside of another Widget.
Upvotes: 1
Reputation: 5255
There is this bug (I'm pretty sure we can call it that since it crashes whole app with meaningless error message) where you cannot return
from the insides of a lambda inside layout composable. So if you do this:
Column {
if (variable){
return
}
Text("test")
}
if the variable
is true - compose hard crashes. I'm trying to find if there is a bug report for that. In the meantime - there are 2 things you can do to avoid that:
You can move your code to function, like this:
@Composable
fun ConnectionScreen(
ip: String,
viewModel: BridgeViewModel,
navController: NavHostController
) {
Scaffold {
Content(viewModel)
}
}
@Composable
fun Content(viewModel: BridgeViewModel) {
val page = viewModel.page.collectAsState()
val loading = viewModel.loading.collectAsState()
val error = viewModel.error.collectAsState()
if (loading.value) {
Loader()
}
error.value?.let {
AlertDialogWithButtons(
title = "Error",
message = it,
onNegativeClick = { },
onPositiveClick = {},
)
}
when (page.value) {
0 -> Screen0()
1 -> Screen1()
2 -> Screen2()
}
{
Or you can stick to if-else
, like this:
@Composable
fun ConnectionScreen(
ip: String,
viewModel: BridgeViewModel,
navController: NavHostController
) {
val page = viewModel.page.collectAsState()
val loading = viewModel.loading.collectAsState()
val error = viewModel.error.collectAsState()
Scaffold {
if (loading.value) {
Loader()
} else {
error.value?.let {
AlertDialogWithButtons(
title = "Error",
message = it,
onNegativeClick = { },
onPositiveClick = {},
)
}
when (page.value) {
0 -> Screen0()
1 -> Screen1()
2 -> Screen2()
}
}
}
}
Upvotes: 0