I want to show a spinning progress bar while shopping list items are being retrieved from the database in the ViewModel and then hide it when the data has been loaded, but the code below throws the following IllegalStateException. How can I fix it?:
java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
Composable
#Composable
fun ShoppingListScreen(
navController: NavHostController,
shoppingListScreenViewModel: ShoppingListScreenViewModel,
sharedViewModel: SharedViewModel
) {
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
val allItems = shoppingListScreenViewModel.shoppingListItemsState.value?.collectAsLazyPagingItems()
val showProgressBar = shoppingListScreenViewModel.loading.value
...
Box {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.height(screenHeight)
) {
if (allItems?.itemCount == 0) {
item { Text("You don't have any items in this shopping list.") }
}
items(
items = allItems!!,
key = { item ->
item.id
}
) { item ->
ShoppingListScreenItem(
navController = navController,
item = item,
sharedViewModel = sharedViewModel
) { isChecked ->
scope.launch {
shoppingListScreenViewModel.changeItemChecked(item!!, isChecked)
}
}
}
item { Spacer(modifier = Modifier.padding(screenHeight - (screenHeight - 70.dp))) }
}
ConditionalCircularProgressBar(isDisplayed = showProgressBar)
}
...
}
ViewModel
#HiltViewModel
class ShoppingListScreenViewModel #Inject constructor(
private val getAllShoppingListItemsUseCase: GetAllShoppingListItemsUseCase
) : ViewModel() {
private val _shoppingListItemsState = mutableStateOf<Flow<PagingData<ShoppingListItem>>?>(null)
val shoppingListItemsState: State<Flow<PagingData<ShoppingListItem>>?> get() = _shoppingListItemsState
val loading = mutableStateOf(false)
init {
loading.value = true
getAllShoppingListItemsFromDb()
}
private fun getAllShoppingListItemsFromDb() {
viewModelScope.launch(Dispatchers.IO){
_shoppingListItemsState.value = getAllShoppingListItemsUseCase().distinctUntilChanged()
withContext(Dispatchers.Main) {
loading.value = false
}
}
}
Related
I am trying to learn compose and retrofit and for that I am developing a very easy app, fetching jokes from a public API and showing them in a lazy list. But it is not working and I am not able to see any jokes. I am new to Kotlin and Jetpack compose. Please help me debug this.
I have a joke class
data class Joke(
val id: Int,.
val punchline: String,
val setup: String,
val type: String
)
This is the API I am GETing from:
https://official-joke-api.appspot.com/jokes/:id
This is the response:
{"type":"general","setup":"What did the fish say when it hit the wall?","punchline":"Dam.","id":1}
This is the retrofit api service:
const val BASE_URL = "https://official-joke-api.appspot.com/"
interface JokeRepository {
#GET("jokes/{id}")
suspend fun getJoke(#Path("id") id: String ) : Joke
companion object {
var apiService: JokeRepository? = null
fun getInstance(): JokeRepository {
if (apiService == null) {
apiService = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build().create(JokeRepository::class.java)
}
return apiService!!
}
}
}
This is the Jokes view model:
class JokeViewModel : ViewModel() {
private val _jokeList = mutableListOf<Joke>()
var errorMessage by mutableStateOf("")
val jokeList: List<Joke> get() = _jokeList
fun getJokeList() {
viewModelScope.launch {
val apiService = JokeRepository.getInstance()
try {
_jokeList.clear()
// for(i in 1..100) {
// var jokeWithId = apiService.getJoke(i.toString())
// _jokeList.add(jokeWithId)
// Log.d("DEBUGGG", jokeWithId.setup)
// }
var joke = apiService.getJoke("1")
_jokeList.add(joke)
}
catch (e: Exception) {
errorMessage = e.message.toString()
}
}
}
}
This is the Main Activity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val jokeViewModel = JokeViewModel()
super.onCreate(savedInstanceState)
setContent {
HasyamTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
JokeView(jvm = jokeViewModel)
}
}
}
}
}
This is the Joke Component and view
#Composable
fun JokeView(jvm: JokeViewModel) {
LaunchedEffect(Unit, block = {
jvm.getJokeList()
})
Text(text = jvm.errorMessage)
LazyColumn() {
items(jvm.jokeList) {
joke -> JokeComponent(joke)
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun JokeComponent(joke: Joke) {
var opened by remember { mutableStateOf(false)}
Column(
modifier = Modifier.padding(15.dp)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { },
elevation = CardDefaults.cardElevation(
defaultElevation = 5.dp
),
onClick = { opened = !opened}
) {
Text(modifier = Modifier.padding(15.dp), text = joke.setup)
}
if (opened) {
Text(modifier = Modifier.padding(15.dp), text = joke.punchline)
}
}
}
Thank you so much
The issue here is that you are not using stateFlow. The screen is not recomposed so your LazyColumn is not recreated with the updated values.
ViewModel
class JokeViewModel : ViewModel() {
var errorMessage by mutableStateOf("")
private val _jokes = MutableStateFlow(emptyList<Joke>())
val jokes = _jokes.asStateFlow()
fun getJokeList() {
viewModelScope.launch {
val apiService = JokeRepository.getInstance()
try {
var jokes = apiService.getJoke("1")
_jokes.update { jokes }
} catch (e: Exception) {
errorMessage = e.message.toString()
}
}
}
}
Joke View
#Composable
fun JokeView(jvm: JokeViewModel) {
val jokes by jvm.jokes.collectAsState()
LaunchedEffect(Unit, block = {
jvm.getJokeList()
})
Text(text = jvm.errorMessage)
LazyColumn {
items(jokes) {
joke -> JokeComponent(joke)
}
}
}
You should read the following documentation about states : https://developer.android.com/jetpack/compose/state
In my application i'm using Navigation Compose. After implementing it, my viewModel's function getExercises() won't fetch the data. Why is that?
MainActivity:
class MainActivity : ComponentActivity() {
lateinit var navController: NavHostController
lateinit var viewModel: ExerciseViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
GymEncV2Theme {
// A surface container using the 'background' color from the theme
navController = rememberNavController()
viewModel = ExerciseViewModel()
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
SetupNavGraph(navController = navController, viewModel = viewModel)
}
}
}
}
}
NavGraph:
#Composable
fun SetupNavGraph(navController: NavHostController, viewModel: ExerciseViewModel) {
NavHost(navController = navController, startDestination = Screen.Home.route) {
composable(
route = Screen.Home.route
) {
HomeScreen(navController = navController)
}
composable(
route = Screen.SampleExercise.route,
arguments = listOf(navArgument(SAMPLE_EXERCISE_SCREEN_KEY) {
type = NavType.StringType
})
) {
SampleExerciseScreen(navController = navController, viewModel = viewModel, muscleGroup = it.arguments?.getString(
SAMPLE_EXERCISE_SCREEN_KEY).toString())
}
}
}
ExerciseApi built with Retrofit:
interface ExerciseApi {
#GET("GymEnc_v2.JSON")
suspend fun getExercises() : List<Exercise>
companion object {
private var exerciseApi: ExerciseApi? = null
fun getInstance() : ExerciseApi {
if (exerciseApi == null) {
exerciseApi = Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ExerciseApi::class.java)
}
return exerciseApi!!
}
}
}
Repository Implementation:
class ExerciseRepositoryImpl : ExerciseRepository {
private val api = ExerciseApi.getInstance()
override suspend fun getExercises(): List<Exercise> {
return api.getExercises()
}
}
And finally the viewModel:
class ExerciseViewModel : ViewModel() {
private val repository = ExerciseRepositoryImpl()
var exerciseListResponse : List<Exercise> by mutableStateOf(arrayListOf())
var errorMessage: String by mutableStateOf("")
fun getExercises(): List<Exercise>{
viewModelScope.launch {
try {
val exerciseList = repository.getExercises()
exerciseListResponse = exerciseList
} catch (e: Exception) {
errorMessage = e.message.toString()
}
}
return exerciseListResponse
}
}
SampleExercisesScreen:
#Composable
fun SampleExerciseScreen(navController: NavController, muscleGroup: String, viewModel: ExerciseViewModel) {
val exerciseList = viewModel.getExercises().filter { it.muscle == muscleGroup }
Surface(modifier = Modifier.fillMaxSize(),
color = AppColors.mBackground) {
Column {
MyTopBar(navController = navController)
Text(text = muscleGroup,
style = MaterialTheme.typography.h3,
color = AppColors.mDetails,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center)
UserExercisesButton()
}
}
}
After using viewModel.getExercises() in SampleExercisesScreen, they simply won't load, leaving me with an empty list.
Depending on the status of PagingData<ShoppingListItem>, I'm updating the loading status in the ViewModel with a sealed class. As I don't know how to check if PagingData<ShoppingListItem> is emtpy in the ViewModel, I'm just collecting the PagingData and calling the .map function on it to add it to a mutableList. I then update the loading state as Emtpy if the list is empty and display an empty message in the UI. The problem is all the code below the collect() method call does not execute, and the circular progress bar does not go away. Even a Log statement below it does not execute. How can I make this work?
Sealed Class
sealed class ListItemsState {
object Loading : ListItemsState()
object Empty : ListItemsState()
object Error : ListItemsState()
data class Success(val allItems: Flow<PagingData<ShoppingListItem>>?) : ListItemsState()
}
ViewModel
#HiltViewModel
class ShoppingListScreenViewModel #Inject constructor(
private val getAllShoppingListItemsUseCase: GetAllShoppingListItemsUseCase
) {
private val _shoppingListItemsState = mutableStateOf<Flow<PagingData<ShoppingListItem>>?>(null)
private val _listItemsLoadingState = MutableStateFlow<ListItemsState>(ListItemsState.Loading)
val listItemsLoadingState = _listItemsLoadingState.asStateFlow()
private val tempList = mutableListOf<ShoppingListItem>()
init {
viewModelScope.launch {
getAllShoppingListItemsFromDb()
}
}
private suspend fun getAllShoppingListItemsFromDb() {
_shoppingListItemsState.value = getAllShoppingListItemsUseCase().distinctUntilChanged()
_shoppingListItemsState.value?.collect {
it.map { shoppingListItem ->
tempList.add(shoppingListItem)
}
} //everything below this line does not execute
Log.i("##sealed", tempList.isEmpty().toString())
when {
tempList.isEmpty() -> _listItemsLoadingState.value = ListItemsState.Empty
else -> _listItemsLoadingState.value =
ListItemsState.Success(_shoppingListItemsState.value)
}
}
}
ShoppingListScreen Composable
#Composable
fun ShoppingListScreen(
navController: NavHostController,
shoppingListScreenViewModel: ShoppingListScreenViewModel,
sharedViewModel: SharedViewModel
) {
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
val allItemsState = shoppingListScreenViewModel.listItemsLoadingState.collectAsState().value
Scaffold(
topBar = {
CustomAppBar(
title = "Shopping List",
titleFontSize = 20.sp,
appBarElevation = 4.dp,
navController = navController
)
},
floatingActionButton = {
FloatingActionButton(
onClick = {
shoppingListScreenViewModel.setStateValue(SHOW_ADD_ITEM_DIALOG_STR, true)
},
backgroundColor = Color.Blue,
contentColor = Color.White
) {
Icon(Icons.Filled.Add, "")
}
},
backgroundColor = Color.White,
// Defaults to false
isFloatingActionButtonDocked = false,
bottomBar = { BottomNavigationBar(navController = navController) }
) {
Box {
when (allItemsState) {
is ListItemsState.Loading -> ConditionalCircularProgressBar(isDisplayed = true)
is ListItemState.Empty -> Text("Empty!")
is ListItemsState.Error -> Text("Error!")
is ListItemsState.Success -> {
ConditionalCircularProgressBar(isDisplayed = false)
val successItems = allItemsState.allItems?.collectAsLazyPagingItems()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.height(screenHeight)
) {
items(
items = successItems!!,
key = { item ->
item.id
}
) { item ->
ShoppingListScreenItem(
navController = navController,
item = item,
sharedViewModel = sharedViewModel
) { isChecked ->
scope.launch {
shoppingListScreenViewModel.changeItemChecked(item!!, isChecked)
}
}
}
item { Spacer(modifier = Modifier.padding(screenHeight - (screenHeight - 70.dp))) }
}
}
else -> {}
}
}
}
}
I have a LazyColumn that collects a Flow<PagingData<ShoppingListItem>>? from the ViewModel, but the itemCount property of the collected state reports an empty list with 0 items at first briefly before it reports the actual number of items in the database. How can I fix this?
Logcat Output
2022-11-17 06:41:06.187 I/##successItemsCount: 0
2022-11-17 06:41:06.447 I/##successItemsCount: 0
2022-11-17 06:41:06.501 I/##successItemsCount: 11
Sealed Class
sealed class ListItemsState {
object Loading : ListItemsState()
object Error : ListItemsState()
data class Success(val allItems: Flow<PagingData<ShoppingListItem>>?) : ListItemsState()
}
ViewModel
#HiltViewModel
class ShoppingListScreenViewModel #Inject constructor(
private val getAllShoppingListItemsUseCase: GetAllShoppingListItemsUseCase
) {
private val _shoppingListItemsState = mutableStateOf<Flow<PagingData<ShoppingListItem>>?>(null)
private val _listItemsLoadingState = MutableStateFlow<ListItemsState>(ListItemsState.Loading)
val listItemsLoadingState = _listItemsLoadingState.asStateFlow()
init {
getAllShoppingListItemsFromDb()
}
private fun getAllShoppingListItemsFromDb() {
viewModelScope.launch {
_shoppingListItemsState.value = getAllShoppingListItemsUseCase().distinctUntilChanged()
_listItemsLoadingState.value = ListItemsState.Success(_shoppingListItemsState.value)
}
}
}
ShoppingListScreen Composable
#Composable
fun ShoppingListScreen(
navController: NavHostController,
shoppingListScreenViewModel: ShoppingListScreenViewModel,
sharedViewModel: SharedViewModel
) {
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
val allItemsState = shoppingListScreenViewModel.listItemsLoadingState.collectAsState().value
Scaffold(
topBar = {
CustomAppBar(
title = "Shopping List",
titleFontSize = 20.sp,
appBarElevation = 4.dp,
navController = navController
)
},
floatingActionButton = {
FloatingActionButton(
onClick = {
shoppingListScreenViewModel.setStateValue(SHOW_ADD_ITEM_DIALOG_STR, true)
},
backgroundColor = Color.Blue,
contentColor = Color.White
) {
Icon(Icons.Filled.Add, "")
}
},
backgroundColor = Color.White,
// Defaults to false
isFloatingActionButtonDocked = false,
bottomBar = { BottomNavigationBar(navController = navController) }
) {
Box {
when (allItemsState) {
is ListItemsState.Loading -> ConditionalCircularProgressBar(isDisplayed = true)
is ListItemsState.Error -> Text("Error!")
is ListItemsState.Success -> {
ConditionalCircularProgressBar(isDisplayed = false)
val successItems = allItemsState.allItems?.collectAsLazyPagingItems()
Log.i("##successItemsCount", successItems.itemCount.toString())
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.height(screenHeight)
) {
items(
items = successItems!!,
key = { item ->
item.id
}
) { item ->
ShoppingListScreenItem(
navController = navController,
item = item,
sharedViewModel = sharedViewModel
) { isChecked ->
scope.launch {
shoppingListScreenViewModel.changeItemChecked(item!!, isChecked)
}
}
}
item { Spacer(modifier = Modifier.padding(screenHeight - (screenHeight - 70.dp))) }
}
}
else -> {}
}
}
}
}
I have this list I am getting from Firebase and displaying in a LazyColumn but it only works when I navigate to a different fragment and press back. My lazyColumn is being composed inside de ScreenController for the bottomNavigation.
What do I have to do to display it directly?
below is my Fragment
#AndroidEntryPoint
class OpenTicketFragment : Fragment() {
private val viewModel: OpenTicketsViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setContent {
val ticketsList = viewModel.tickets.value.data
val currentUserId = viewModel.currentUserId.value
TheProjectTheme {
val navController = rememberNavController()
val title = remember { mutableStateOf("Open Tickets") }
val isDialogOpen = remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = {
isDialogOpen.value = true
}) {
Icon(Icons.Default.ExitToApp, contentDescription = null)
}
},
title = {
Text(text = title.value)
},
actions = {
IconButton(onClick = {
findNavController().navigate(R.id.action_openTicketFragment_to_profileFragment)
}) {
Icon(Icons.Default.Person, contentDescription = null)
}
}
)
},
bottomBar = {
val items = listOf(
Screen.Open,
Screen.Available,
Screen.Closed
)
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute =
navBackStackEntry?.arguments?.getString(KEY_ROUTE)
items.forEach {
BottomNavigationItem(
icon = { Icon(it.icon, contentDescription = null) },
selected = currentRoute == it.route,
label = { Text(text = it.label) },
onClick = {
navController.popBackStack(
navController.graph.startDestination, false
)
if (currentRoute != it.route) {
navController.navigate(it.route)
}
})
}
}
}
)
{
ScreenController(
navHostController = navController,
title,
ticketsList!!,
findNavController(),
currentUserId
)
AlertDialogComponent(isDialogOpen, findNavController(), viewModel)
}
}
}
}
}
}
#Composable
fun OpenTicketLazyColumn(
ticket: Ticket,
// onClick: () -> Unit,
navController: NavController,
) {
Card(
modifier = Modifier
.padding(8.dp)
.clickable {
val action =
OpenTicketFragmentDirections.actionOpenTicketFragmentToTicketDetailFragment(
ticket.ticketId,
ticket.problem,
ticket.address,
ticket.dateOpened,
ticket.description,
ticket.name,
ticket.status
)
Log.d("SSS8", ticket.ticketId)
navController.navigate(action)
},
elevation = 4.dp
) {
Row(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Column(
modifier = Modifier
.padding(8.dp)
.weight(1f)
) {
Text(
text = "Ticket Opened Date: ${ticket.dateOpened}",
fontSize = 16.sp,
)
Text(
text = "Service required: ${ticket.problem}",
fontSize = 14.sp,
)
}
Column(
modifier = Modifier
.padding(8.dp)
.weight(1f)
) {
Text(
text = "Status: ${ticket.status}",
fontSize = 16.sp,
)
Text(
text = "Address: ${ticket.address}",
fontSize = 16.sp,
)
}
}
}
}
and my composables
#Composable
fun ScreenController(
navHostController: NavHostController,
topBarTitle: MutableState<String>,
ticketList: List<Ticket>,
navController: NavController,
currentUserId: String
) {
NavHost(
navController = navHostController, startDestination = "open"
) {
composable("open") {
Log.d("SACO", currentUserId)
LazyColumn(
modifier = Modifier
.padding(8.dp)
) {
items(items = ticketList) { item ->
val status = remember { mutableStateOf(item.status)}
if (status.value == ASSIGNED && item.assignedToId == currentUserId) {
OpenTickets(
item,
navController
)
}
}
}
topBarTitle.value = "Open Tickets"
}
composable("available") {
LazyColumn(
modifier = Modifier
.padding(8.dp)
) {
items(items = ticketList) { item ->
val status = remember { mutableStateOf(item.status)}
if (status.value == OPEN) {
ClosedTickets(
item,
navController
)
}
}
}
topBarTitle.value = "Available Tickets"
}
composable("closed") {
LazyColumn(
modifier = Modifier
.padding(8.dp)
) {
items(items = ticketList) { item ->
val status = remember { mutableStateOf(item.status)}
if (status.value == CLOSED && item.assignedToId == currentUserId) {
AvailableTickets(
item,
navController
)
}
}
}
topBarTitle.value = "Closed Tickets"
}
}
}
#Composable
fun OpenTickets(
ticket: Ticket,
navController: NavController
) {
OpenTicketLazyColumn(
ticket,
navController
)
}
the viewModel
#HiltViewModel
class OpenTicketsViewModel #Inject constructor(
private val repository: WorkerRepositoryImpl,
private val firesource: FireBaseSource
) : ViewModel() {
val currentUserId = mutableStateOf("")
val tickets: MutableState<DataOrException<List<Ticket>, Exception>> = mutableStateOf(
DataOrException(
listOf(),
Exception("")
)
)
init {
getAllTickets(listOfServices)
}
private fun getAllTickets() {
viewModelScope.launch {
val ticketsList = repository.getAllTickets().data
tickets.value.data = ticketsList
}
}
the firebaseSource
class FireBaseSource #Inject constructor(
private val firebaseAuth: FirebaseAuth,
private val firestore: FirebaseFirestore
) {
suspend fun getAllTickets(
listOfServices: List<String>
): DataOrException<List<Ticket>, Exception> {
val dataOrException = DataOrException<List<Ticket>, Exception>()
try {
dataOrException.data = firestore.collection(TICKETS)
.whereEqualTo(PROBLEM, ELECTRICIAN)
.get()
.await().map { document ->
document.toObject(Ticket::class.java)
}
} catch (e: FirebaseFirestoreException) {
dataOrException.e = e
}
return dataOrException
}
}
1. List item