I am developing an application in which I show a list of potatoes, retrieving the data from Firestore.
I have added a swipe action to refresh the data. With the code that I show below, the data is updating fine, the call is made to Firestore and it is updated showing new values in case they exist, or stopping showing values that no longer exist.
The problem is that when I swipe the potato list screen remains blank, empty, and when the call to Firestore ends, they are shown again. That is, there are a couple of seconds that the screen goes blank.
Is there a possibility that this will not happen? This effect is somewhat ugly
ViewModel:
#HiltViewModel
class PotatoesViewModel #Inject constructor(
private val getPotatoesDataUseCase: GetPotatoesData
) : ViewModel() {
private val _state = mutableStateOf(PotatoesState())
val state: State<PotatoesState> = _state
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow<Boolean>
get() = _isRefreshing.asStateFlow()
init {
getPotatoes()
}
private fun getPotatoes() {
getPotatoesDataUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = PotatoesState(potatoes = result.data?.potatoes ?: emptyList())
}
is Resource.Error -> {
_state.value = PotatoesState(
error = result.message ?: "An unexpected error occurred"
)
}
is Resource.Loading -> {
_state.value = PotatoesState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
fun refresh() {
viewModelScope.launch {
_isRefreshing.emit(true)
getIncidents()
_isRefreshing.emit(false)
}
}
}
Screen:
#Composable
fun PotatoesDataScreen(
navController: NavController,
viewModel: PotatoesViewModel = hiltViewModel()
) {
val state = viewModel.state.value
val isRefreshing by viewModel.isRefreshing.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
stringResource(R.string.app_name),
fontWeight = FontWeight.Bold
)
},
backgroundColor = Primary,
contentColor = Color.White
)
},
content = {
Box(modifier = Modifier.fillMaxSize()) {
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { viewModel.refresh() }
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 8.dp)
) {
items(state.potatoes) { potato ->
PotatoCard(
potato = potato
)
}
}
}
}
}
)
}
PotatoState:
data class PotatoesState(
val isLoading: Boolean = false,
val potatoes: List<Potato> = emptyList(),
val error: String = ""
)
When the list screen is blank, this is the time when the Api call is made.
When your call is made and response is still not received, this is also when the list is blank.
You pass a new Object of PotatoesState to the mutableState every time you:
receive a response,
get an error, (with Potatoes = emptyList())
or state is loading. (with Potatoes = emptyList())
UI is updated according to the MutableState you named _state.
If you want to keep the same data until you get a new response, then you need to update the current state.value: MutableState<PotatoesState> object only when you get a new response (AKA, is Resource.success).
Alternatively, you can implement a Loading Spinner, and show it when you start your Api Request, until isLoading is false.
EDIT: Addition of suggested code.
So that how I would declare PotatoesState class:
class PotatoesState(
var isLoading: Boolean = false,
var potatoes: List<Potato> = emptyList(),
var error: String = ""
)
And that's what I would write in the CallBack function:
when (result) {
is Resource.Success -> {
if (result.data != null) {
_state.value = mutableStateOf().apply {
addAll(_state.value.apply { it ->
it.potatoes = result.data!!.potatoes
it.isLoading = false
it.error = ""
})
}
}
}
is Resource.Error -> {
_state.value = mutableStateOf().apply {
addAll(_state.value.apply { it ->
it.error =
result.message ?:
"An unexpected error occurred"
it.isLoading = false
})
}
}
is Resource.Loading -> {
_state.value = mutableStateOf().apply {
addAll(_state.value.apply { it ->
it.isLoading = true
})
}
}
}
Related
I have implemented search functionality in my app which display result as a verticalGridView with pagination : https://github.com/alirezaeiii/TMDb-Compose
I have following logic for refresh load state that works as I wish :
#Composable
fun <T : TMDbItem> PagingScreen(
viewModel: BasePagingViewModel<T>,
onClick: (TMDbItem) -> Unit,
) {
val lazyTMDbItems = viewModel.pagingDataFlow.collectAsLazyPagingItems()
when (lazyTMDbItems.loadState.refresh) {
is LoadState.Loading -> {
TMDbProgressBar()
}
is LoadState.Error -> {
val message =
(lazyTMDbItems.loadState.refresh as? LoadState.Error)?.error?.message ?: return
lazyTMDbItems.apply {
ErrorScreen(
message = message,
modifier = Modifier.fillMaxSize(),
refresh = { retry() }
)
}
}
else -> {
LazyTMDbItemGrid(lazyTMDbItems, onClick)
}
}
}
In LazyTMDbItemGrid, I try to manage append load state as follow :
#Composable
private fun <T : TMDbItem> LazyTMDbItemGrid(
lazyTMDbItems: LazyPagingItems<T>,
onClick: (TMDbItem) -> Unit,
) {
LazyVerticalGrid(
columns = GridCells.Fixed(COLUMN_COUNT),
contentPadding = PaddingValues(
start = Dimens.GridSpacing,
end = Dimens.GridSpacing,
bottom = WindowInsets.navigationBars.getBottom(LocalDensity.current)
.toDp().dp.plus(
Dimens.GridSpacing
)
),
horizontalArrangement = Arrangement.spacedBy(
Dimens.GridSpacing,
Alignment.CenterHorizontally
),
content = {
repeat(COLUMN_COUNT) {
item {
Spacer(
Modifier.windowInsetsTopHeight(
WindowInsets.statusBars.add(WindowInsets(top = 56.dp))
)
)
}
}
items(lazyTMDbItems.itemCount) { index ->
val tmdbItem = lazyTMDbItems[index]
tmdbItem?.let {
TMDbItemContent(
it,
Modifier
.height(320.dp)
.padding(vertical = Dimens.GridSpacing),
onClick
)
}
}
lazyTMDbItems.apply {
when (loadState.append) {
is LoadState.Loading -> {
item(span = span) {
LoadingRow(modifier = Modifier.padding(vertical = Dimens.GridSpacing))
}
}
is LoadState.Error -> {
val message =
(loadState.append as? LoadState.Error)?.error?.message ?: return#apply
item(span = span) {
ErrorScreen(
message = message,
modifier = Modifier.padding(vertical = Dimens.GridSpacing),
refresh = { retry() })
}
}
else -> {}
}
}
})
}
The problem is when there is no result for search, or when result items is shorter than screen size, it displays LoadingRow. My expectation is when we are in this state, LoadingRow does not display, but how can I detect this state?
Correct me if I'm wrong but these should be dictated by the PagingSource.LoadResult.Page
Documentation :
Success result object for PagingSource.load. Params: data - Loaded
data prevKey - Key for previous page if more data can be loaded in
that direction, null otherwise. nextKey - Key for next page if more
data can be loaded in that direction, null otherwise.
So if you reached the pagination end (in either direction) :
PagingSource.LoadResult.Page(
data = loadedData,
prevKey = null,
nextKey = null)
I have debugged the app and I saw that the data in UIState changes when I try to add or remove the item, especially the isAdded field. However, even though the isAdded changes, the AddableItem does not recompose. Additionally, when I try to sort items, or try to write a query THAT WILL NOT SEND ANY API REQUEST, JUST CHANGES THE STRING IN TEXTFIELD, the UI recomposes. So UI reacts to changes in UIState. I have searched for similar issues but cannot find anything. I believe that the framework must recompose when the pointer of the filed changes, however, it does not. Any idea why this happens or solve that?
This is the viewModel:
#HiltViewModel
class AddableItemScreenViewModel#Inject constructor(
val getAddableItemsUseCase: GetItems,
val getItemsFromRoomUseCase: GetRoomItems,
val updateItemCase: UpdateItem,
savedStateHandle: SavedStateHandle) : ViewModel() {
private val _uiState = mutableStateOf(UIState())
val uiState: State<UIState> = _uiState
private val _title = mutableStateOf("")
val title: State<String> = _title
private var getItemsJob: Job? = null
init {
savedStateHandle.get<String>(NavigationConstants.TITLE)?.let { title ->
_title.value = title
}
savedStateHandle.get<Int>(NavigationConstants.ID)?.let { id ->
getItems(id = id.toString())
}
}
fun onEvent(event: ItemEvent) {
when(event) {
is ItemEvent.UpdateEvent -> {
val modelToUpdate = UpdateModel(
id = event.source.id,
isAdded = event.source.isAdded,
name = event.source.name,
index = event.source.index
)
updateUseCase(modelToUpdate).launchIn(viewModelScope)
}
is ItemEvent.QueryChangeEvent -> {
_uiState.value = _uiState.value.copy(
searchQuery = event.newQuery
)
}
is ItemEvent.SortEvent -> {
val curSortType = _uiState.value.sortType
_uiState.value = _uiState.value.copy(
sortType = if(curSortType == SortType.AS_IT_IS)
SortType.ALPHA_NUMERIC
else
SortType.AS_IT_IS
)
}
}
}
private fun getItems(id: String) {
getItemsJob?.cancel()
getItemsJob = getItemsUseCase(id)
.combine(
getItemsFromRoomUseCase()
){ itemsApiResult, roomData ->
when (itemsApiResult) {
is Resource.Success -> {
val data = itemsApiResult.data.toMutableList()
// Look the api result, if the item is added on room, make it added, else make it not added. This ensures API call is done once and every state change happens because of room.
for(i in data.indices) {
val source = data[i]
val itemInRoomData = roomData.find { it.id == source.id }
data[i] = data[i].copy(
isAdded = itemInRoomData != null
)
}
_uiState.value = _uiState.value.copy(
data = data,
isLoading = false,
error = "",
)
}
is Resource.Error -> {
_uiState.value = UIState(
data = emptyList(),
isLoading = false,
error = itemsApiResult.message,
)
}
is Resource.Loading -> {
_uiState.value = UIState(
data = emptyList(),
isLoading = true,
error = "",
)
}
}
}.launchIn(viewModelScope)
}
}
This it the composable:
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun AddableItemsScreen(
itemsViewModel: AddableItemScreenViewModel = hiltViewModel()
) {
val state = itemsViewModel.uiState.value
val controller = LocalNavigationManager.current
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val mainScrollState = rememberLazyListState()
val focusRequester = remember { FocusRequester() }
// Screen UI
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.BackgroundColor)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
focusManager.clearFocus()
},
) {
LazyColumn(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
state = mainScrollState,
) {
item {
WhiteSpacer(
whiteSpacePx = 200,
direction = SpacerDirections.VERTICAL
)
}
if (state.isLoading) {
item {
ProgressIndicator()
}
}
if (state.error.isNotEmpty() && state.error.isNotBlank()) {
item {
ErrorText()
}
}
if (state.data.isNotEmpty()) {
val data = if (state.sortType == SortType.ALPHA_NUMERIC)
state.data.sortedBy { it.name }
else
state.data
data.forEach { source ->
if((state.searchQuery.isEmpty() && state.searchQuery.isBlank()) ||
(source.name != null && source.name.contains(state.searchQuery, ignoreCase = true))) {
item {
AddableItem(
modifier = Modifier
.padding(
vertical = dimManager.heightPxToDp(20)
),
text = source.name ?: "",
isAdded = source.isAdded ?: false,
onItemPressed = {
controller.navigate(
Screens.ItemPreviewScreen.route +
"?title=${source.name}" +
"&id=${source.categoryId}" +
"&isAdded=${source.isAdded}"
)
},
onAddPressed = {
itemsViewModel.onEvent(ItemEvent.UpdateEvent(source))
}
)
}
}
}
}
}
Column(
modifier = Modifier
.align(Alignment.TopStart)
.background(
MaterialTheme.colors.BackgroundColor
),
) {
ItemsScreenAppBar(
title = itemsViewModel.title.value,
onSortPressed = {
itemsViewModel.onEvent(ItemEvent.SortEvent)
}
) {
controller.popBackStack()
}
SearchBar(
query = state.searchQuery,
focusRequester = focusRequester,
placeholder = itemsViewModel.title.value,
onDeletePressed = {
itemsViewModel.onEvent(ItemEvent.QueryChangeEvent(""))
},
onValueChanged = {
itemsViewModel.onEvent(ItemEvent.QueryChangeEvent(it))
},
onSearch = {
keyboardController!!.hide()
}
)
WhiteSpacer(
whiteSpacePx = 4,
direction = SpacerDirections.VERTICAL
)
}
}
}
And finally this is the UIState:
data class UIState(
val data: List<ItemModel> = emptyList(),
val isLoading: Boolean = false,
val error: String = "",
val searchQuery: String = "",
val sortType: SortType = SortType.AS_IT_IS,
)
#Parcelize
data class ItemModel (
val id: Int? = null,
var isAdded: Boolean? = null,
val name: String? = null,
val index: Int? = null,
#SerializedName("someSerializedNameForApi")
var id: Int? = null
): Parcelable
Finally, I have a similar issue with almost the same viewModel with the same UI structure. The UI contains an Add All button and when everything is added, it turns to Remove All. I also hold the state of the button in UIState for that screen. When I try to add all items or remove all items, the UI recomposes. But when I try to add or remove a single item, the recomposition does not happen as same as the published code above. Additionally, when I remove one item when everything is added on that screen, the state of the button does change but stops to react when I try to add more. I can also share that code if you people want. I still do not understand why the UI recomposes when I try to sort or try to add-remove all on both screens but does not recompose when the data changes, even though I change the pointer address of the list.
Thanks for any help.
I could not believe that the answer can be so simple but here are the solutions:
For the posted screen, I just changed _uiState.value = _uiState.value.copy(...) to _uiState.value = UIState(...copy and replace everything with old value...) as
_uiState.value = UIState(
data = data,
isLoading = false,
error = "",
searchQuery = _uiState.value.searchQuery,
sortType = _uiState.value.sortType
)
For the second screen, I was just double changing the isAdded value by sending the data directly without copying. As the api call changes the isAdded value again, and the read from room flow changes it again, the state were changed twice.
However, I still wonder why compose didn't recompose when I changed the memory location of data in UIState.
I am trying to do pagination in my application. First, I'm fetching 20 item from Api (limit) and every time i scroll down to the bottom of the screen, it increase this number by 20 (nextPage()). However, when this function is called, the screen goes to the top, but I want it to continue where it left off. How can I do that?
Here is my code:
CharacterListScreen:
#Composable
fun CharacterListScreen(
characterListViewModel: CharacterListViewModel = hiltViewModel()
) {
val state = characterListViewModel.state.value
val limit = characterListViewModel.limit.value
Box(modifier = Modifier.fillMaxSize()) {
val listState = rememberLazyListState()
LazyColumn(modifier = Modifier.fillMaxSize(), state = listState) {
itemsIndexed(state.characters) { index, character ->
characterListViewModel.onChangeRecipeScrollPosition(index)
if ((index + 1) >= limit) {
characterListViewModel.nextPage()
}
CharacterListItem(character = character)
}
}
if (state.error.isNotBlank()) {
Text(
text = state.error,
color = MaterialTheme.colors.error,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.align(Alignment.Center)
)
}
if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
}
CharacterListViewModel:
#HiltViewModel
class CharacterListViewModel #Inject constructor(
private val characterRepository: CharacterRepository
) : ViewModel() {
val state = mutableStateOf(CharacterListState())
val limit = mutableStateOf(20)
var recipeListScrollPosition = 0
init {
getCharacters(limit.value, Constants.HEADER)
}
private fun getCharacters(limit : Int, header : String) {
characterRepository.getCharacters(limit, header).onEach { result ->
when(result) {
is Resource.Success -> {
state.value = CharacterListState(characters = result.data ?: emptyList())
}
is Resource.Error -> {
state.value = CharacterListState(error = result.message ?: "Unexpected Error")
}
is Resource.Loading -> {
state.value = CharacterListState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
private fun incrementLimit() {
limit.value = limit.value + 20
}
fun onChangeRecipeScrollPosition(position: Int){
recipeListScrollPosition = position
}
fun nextPage() {
if((recipeListScrollPosition + 1) >= limit.value) {
incrementLimit()
characterRepository.getCharacters(limit.value, Constants.HEADER).onEach {result ->
when(result) {
is Resource.Success -> {
state.value = CharacterListState(characters = result.data ?: emptyList())
}
is Resource.Error -> {
state.value = CharacterListState(error = result.message ?: "Unexpected Error")
}
is Resource.Loading -> {
state.value = CharacterListState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
}
}
CharacterListState:
data class CharacterListState(
val isLoading : Boolean = false,
var characters : List<Character> = emptyList(),
val error : String = ""
)
I think the issue here is that you are creating CharacterListState(isLoading = true) while loading. This creates an object with empty list of elements. So compose renders an empty LazyColumn here which resets the scroll state. The easy solution for that could be state.value = state.value.copy(isLoading = true). Then, while loading, the item list can be preserved (and so is the scroll state)
Not sure if you are using the LazyListState correctly. In your viewmodel, create an instance of LazyListState:
val lazyListState: LazyListState = LazyListState()
Pass that into your composable and use it as follows:
#Composable
fun CharacterListScreen(
characterListViewModel: CharacterListViewModel = hiltViewModel()
) {
val limit = characterListViewModel.limit.value
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize(), state = characterListViewModel.lazyListState) {
itemsIndexed(state.characters) { index, character ->
}
}
}
}
In my viewModel i get result.data:
accounts: [Account(accountId=461715f3-038c-4c3d-ac3c-1fac44f37f14, currency=GBP, description=Personal, nickname=Sydney Beard)]
Here is the code of ViewModel:
#HiltViewModel
class AccountListViewModel #Inject constructor(
private val getAccountsUseCase: GetAccountsUseCase,
savedStateHandle: SavedStateHandle
): ViewModel() {
private val _state = mutableStateOf<AccountListState>(AccountListState())
val state: State<AccountListState> = _state
init {
savedStateHandle.get<String>(Constants.ACCESS_TOKEN)?.let { accessToken ->
getAccounts(accessToken = accessToken)
}
}
private fun getAccounts(accessToken: String) {
getAccountsUseCase(accessToken = accessToken).onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = AccountListState(accounts = result.data ?: emptyList())
}
is Resource.Error -> {
_state.value = AccountListState(error = result.message ?: "Something went wrong")
}
is Resource.Loading -> {
_state.value = AccountListState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
}
In my screen in log i see that i get account:
account: Account(accountId=461715f3-038c-4c3d-ac3c-1fac44f37f14, currency=GBP, description=Personal, nickname=Sydney Beard)
Here is code of my screen:
#ExperimentalMaterialApi
#Composable
fun OverviewScreen(
navController: NavController,
viewModel: AccountListViewModel = hiltViewModel()
) {
val state = viewModel.state.value
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(state.accounts) { account ->
Log.d("some", "account: $account")
AccountListItem(
account = account,
onItemClick = {
navController.navigate(Screen.AccountDetail.route + "/${account.accountId}")
}
)
}
}
if (state.error.isNotBlank()) {
Text(
text = state.error,
color = MaterialTheme.colors.error,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.align(Alignment.Center)
)
}
if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
}
But unfortunately I can't see anything on the screen, although this is how I display my account.
Q: how to display my account, because now many times I have the getAccounts method called which is why the screen is redrawn every time
UPDATE
I check my AccountListViewModel instance and in method getAccounts() put the next log:
Log.d("some", "getAccounts: ${result.data} vm: ${this.toString()}")
And see that and I saw that every time the view model is recreated:
In Compose you have to collect your flow as a state. You are only reading flow once in your code.
You are also using state in ViewModel which is bad. Use flow there.
Fixes in ViewModel:
// wrong
private val _state = mutableStateOf<AccountListState>(AccountListState())
val state: State<AccountListState> = _state
// correct
private val _state = MutableStateFlow<AccountListState>(AccountListState())
val state: StateFlow<AccountListState> = _state
And fixed in compose:
// wrong
val state = viewModel.state.value
// correct
val state by viewModel.state.collectAsState()
Read more here: https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#(kotlinx.coroutines.flow.StateFlow).collectAsState(kotlin.coroutines.CoroutineContext)
The following Code A is from the official Advanced State in Jetpack Compose Codelab.
And I have read the article.
In Code B, I think the value of uiState.isLoading should always false because either DetailsUiState(cityDetailsResult.data) or DetailsUiState(throwError = true) will get the object with the value isLoading = false by default, right?
I think that the business logic should be this:
Loading screen is displayed first and uiState.isLoading==true when the data ExploreModel is loading.
The data UI is displayed automatically and uiState.cityDetails != null when the data ExploreModel has been loaded.
I run and test the Code A, the log of the project record "Is Loading" first, then record "Have Data".
But I can't understand how the code in the project can display loading screen first, then display data UI automatically again, could you tell me?
Code B
#Composable
fun DetailsScreen(
...
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
...
}
Code A
data class DetailsUiState(
val cityDetails: ExploreModel? = null,
val isLoading: Boolean = false,
val throwError: Boolean = false
)
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
when {
uiState.cityDetails != null -> {
Log.e("My","Have Data") //I add
DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
}
uiState.isLoading -> {
Box(modifier.fillMaxSize()) {
Log.e("My","Is Loading") // I add
CircularProgressIndicator(
color = MaterialTheme.colors.onSurface,
modifier = Modifier.align(Alignment.Center)
)
}
}
else -> { onErrorLoading() }
}
}
#Composable
fun DetailsContent(
exploreModel: ExploreModel,
modifier: Modifier = Modifier
) {
Column(modifier = modifier, verticalArrangement = Arrangement.Center) {
...
}
}
class DetailsViewModel #Inject constructor(
private val destinationsRepository: DestinationsRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val cityName = savedStateHandle.get<String>(KEY_ARG_DETAILS_CITY_NAME)!!
val cityDetails: Result<ExploreModel>
get() {
val destination = destinationsRepository.getDestination(cityName)
return if (destination != null) {
Result.Success(destination)
} else {
Result.Error(IllegalArgumentException("City doesn't exist"))
}
}
}
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
The below line produces the initial State with isLoading true.
val uiState by produceState(initialValue = DetailsUiState(isLoading = true))
This is the initial state with isLoading true.
DetailsUiState(
cityDetails =null,
isLoading = true,
throwError = false
)
So initially It will show a CircularProgressBar.
Then the following code block will be executed.
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
And either it will get data or produce an error.
If it is a success then the state will be like
DetailsUiState(
cityDetails = cityDetailsResult.data,
isLoading = false,
throwError = false
)
or Error
DetailsUiState(
cityDetails = null,
isLoading = false,
throwError = true
)
In both success or error cases, isLoading is false so there will be no CircularProgressBar. Only the initial state will be with isLoading true. So initially, it will show a CircularProgressBar.