I want to display a loading indicator when I download data from the API. However, when this happens, the indicator often stops. How can I change this or what could be wrong? Basically, I fetch departure times and process them (E.g. I convert hex colors to Jetpack Compose color, or unix dates to Date type, etc.) and then load them into a list and display them.
#Composable
fun StopScreen(
unixDate: Long? = null,
stopId: String,
viewModel: MainViewModel = hiltViewModel()
) {
LaunchedEffect(Unit) {
viewModel.getArrivalsAndDeparturesForStop(
unixDate,
stopId,
false
)
}
val isLoading by remember { viewModel.isLoading }
if (!isLoading) {
//showData
} else {
LoadingView()
}
}
#Composable
fun LoadingView() {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
CircularProgressIndicator(color = MaterialTheme.colors.primary)
}
}
And the viewmodel where I process the data:
#HiltViewModel
class MainViewModel #Inject constructor(
private val mainRepository: MainRepository
) : ViewModel() {
var stopTimesList = mutableStateOf<MutableList<StopTime>>(arrayListOf())
var alertsList = mutableStateOf<MutableList<Alert>>(arrayListOf())
var loadError = mutableStateOf("")
var isLoading = mutableStateOf(false)
var isRefreshing = mutableStateOf(false)
fun getArrivalsAndDeparturesForStop(unixDate: Long? = null, stopId: String, refresh: Boolean) {
viewModelScope.launch {
if (refresh) {
isRefreshing.value = true
} else {
isLoading.value = true
}
val result = mainRepository.getArrivalsAndDeparturesForStop(stopId = stopId, time = unixDate)
when (result) {
is Resource.Success -> {
//I temporarily store the data here, so that the screen is only refreshed on reload when all the new data has arrived and loaded
var preStopTimes: MutableList<StopTime> = arrayListOf()
var preAlertsList: MutableList<Alert> = arrayListOf()
if (result.data!!.stopTimes != null && result.data!!.alerts != null) {
var count = 0
val countAll =
result.data!!.stopTimes!!.count() + result.data!!.alertIds!!.count()
if (countAll == 0) {
loadError.value = ""
isLoading.value = false
isRefreshing.value = false
}
//ALERTS
for (alert in result.data!!.data.alerts) {
preAlertsList.add(alert)
count += 1
if (count == countAll) {
stopTimesList.value = preStopTimes
alertsList.value = preAlertsList
loadError.value = ""
isLoading.value = false
isRefreshing.value = false
}
}
for (stopTime in result.data!!.stopTimes!!) {
preStopTimes.add(stopTime)
count += 1
if (count == countAll) {
stopTimesList.value = preStopTimes
alertsList.value = preAlertsList
loadError.value = ""
isLoading.value = false
isRefreshing.value = false
}
}
} else {
loadError.value = "Error"
isLoading.value = false
isRefreshing.value = false
}
}
is Resource.Error -> {
loadError.value = result.message!!
isLoading.value = false
isRefreshing.value = false
}
}
}
}
}
Repository:
#ActivityScoped
class MainRepository #Inject constructor(
private val api: MainApi
) {
suspend fun getArrivalsAndDeparturesForStop(stopId: String,time: Long? = null): Resource<ArrivalsAndDeparturesForStop> {
val response = try {
api.getArrivalsAndDeparturesForStop(
stopId,
time
)
} catch (e: Exception) { return Resource.Error(e.message!!)}
return Resource.Success(response)
}
}
My take is that your Composable recomposes way too often. Since you're updating your state within your for loops. Otherwise, it might be because your suspend method in your MainRepository is not dispatched in the right thread.
I feel you didn't yet grasp how Compose works internally (and that's fine, it's a new topic anyway). I'd recommend hoisting a unique state instead of having several mutable states for all your properties. Then build it internally in your VM to then notify the view when the state changes.
Something like this:
data class YourViewState(
val stopTimesList: List<StopTime> = emptyList(),
val alertsList: List<Alert> = emptyList(),
val isLoading: Boolean = false,
val isRefreshing: Boolean = false,
val loadError: String? = null,
)
#HiltViewModel
class MainViewModel #Inject constructor(
private val mainRepository: MainRepository
) : ViewModel() {
var viewState by mutableStateOf<YourViewState>(YourViewState())
fun getArrivalsAndDeparturesForStop(unixDate: Long? = null, stopId: String, refresh: Boolean) {
viewModelScope.launch {
viewState = if (refresh) {
viewState.copy(isRefreshing = true)
} else {
viewState.copy(isLoading = true)
}
when (val result = mainRepository.getArrivalsAndDeparturesForStop(stopId = stopId, time = unixDate)) {
is Resource.Success -> {
//I temporarily store the data here, so that the screen is only refreshed on reload when all the new data has arrived and loaded
val preStopTimes: MutableList<StopTime> = arrayListOf()
val preAlertsList: MutableList<Alert> = arrayListOf()
if (result.data!!.stopTimes != null && result.data!!.alerts != null) {
var count = 0
val countAll = result.data!!.stopTimes!!.count() + result.data!!.alertIds!!.count()
if (countAll == 0) {
viewState = viewState.copy(isLoading = false, isRefreshing = false)
}
//ALERTS
for (alert in result.data!!.data.alerts) {
preAlertsList.add(alert)
count += 1
if (count == countAll) {
break
}
}
for (stopTime in result.data!!.stopTimes!!) {
preStopTimes.add(stopTime)
count += 1
if (count == countAll) {
break
}
}
viewState = viewState.copy(isLoading = false, isRefreshing = false, stopTimesList = preStopTimes, alertsList = preAlertsList)
} else {
viewState = viewState.copy(isLoading = false, isRefreshing = false, loadError = "Error")
}
}
is Resource.Error -> {
viewState = viewState.copy(isLoading = false, isRefreshing = false, loadError = result.message!!)
}
}
}
}
}
#Composable
fun StopScreen(
unixDate: Long? = null,
stopId: String,
viewModel: MainViewModel = hiltViewModel()
) {
LaunchedEffect(Unit) {
viewModel.getArrivalsAndDeparturesForStop(
unixDate,
stopId,
false
)
}
if (viewModel.viewState.isLoading) {
LoadingView()
} else {
//showData
}
}
Note that I've made a few improvements while keeping the original structure.
EDIT:
You need to make your suspend method from your MainRepository main-safe. It's likely it runs on the main thread (caller thread) because you don't specify on which dispatcher the coroutine runs.
suspend fun getArrivalsAndDeparturesForStop(stopId: String,time: Long? = null): Resource<ArrivalsAndDeparturesForStop> = withContext(Dispatchers.IO) {
try {
api.getArrivalsAndDeparturesForStop(
stopId,
time
)
Resource.Success(response)
} catch (e: Exception) {
Resource.Error(e.message!!)
}
After 6 months I figured out the exact solution. Everything was running on the main thread when I processed the data in the ViewModel. Looking into things further, I should have used Dispatchers.Default and / or Dispatchers.IO within the functions for CPU intensive / list soring / JSON parsing tasks.
https://developer.android.com/kotlin/coroutines/coroutines-adv
suspend fun doSmg() {
withContext(Dispatchers.IO) {
//This dispatcher is optimized to perform disk or network I/O outside of the main thread. Examples include using the Room component, reading from or writing to files, and running any network operations.
}
withContext(Dispatchers.Default) {
//This dispatcher is optimized to perform CPU-intensive work outside of the main thread. Example use cases include sorting a list and parsing JSON.
}
}
Related
I am trying to poplulate values from a datastore. I only want to recover them from the datastore once, which is why I am canceling the job after 1 second. This prevents it constantly updating.
This does not work. (1)
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
val job = Job()
val scope = CoroutineScope(job + Dispatchers.IO)
scope.launch {
tempDataStore.getDieOne.collect {
die1.value = it!!.toInt()
}
tempDataStore.getDisplayText.collect {
displayText.value = it!!
}
tempDataStore.getDieTwo.collect {
die2.value = it!!.toInt()
}
}
delay(1000L)
job.cancel()
}
This does not work. (2)
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
val job = Job()
val scope = CoroutineScope(job + Dispatchers.IO)
scope.launch {
tempDataStore.getDieOne.collect {
die1.value = it!!.toInt()
}
}
scope.launch {
tempDataStore.getDisplayText.collect {
displayText.value = it!!
}
}
scope.launch {
tempDataStore.getDieTwo.collect {
die2.value = it!!.toInt()
}
}
delay(1000L)
job.cancel()
}
This does work! (3)
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
val job = Job()
val job2 = Job()
val job3 = Job()
val scope = CoroutineScope(job + Dispatchers.IO)
val scope2 = CoroutineScope(job2 + Dispatchers.IO)
val scope3 = CoroutineScope(job3 + Dispatchers.IO)
scope.launch {
tempDataStore.getDieOne.collect {
die1.value = it!!.toInt()
}
}
scope2.launch {
tempDataStore.getDisplayText.collect {
displayText.value = it!!
}
}
scope3.launch {
tempDataStore.getDieTwo.collect {
die2.value = it!!.toInt()
}
}
delay(1000L)
job.cancel()
job2.cancel()
job3.cancel()
}
Here is the TempDataStore class (4)
class TempDataStore(private val context: Context) {
companion object{
private val Context.dataStore by preferencesDataStore(name = "TempDataStore")
val DISPLAY_TEXT_KEY = stringPreferencesKey("display_text")
val DIE_ONE = stringPreferencesKey("die_one")
val DIE_TWO = stringPreferencesKey("die_two")
}
val getDisplayText: Flow<String?> = context.dataStore.data
.map { preferences ->
preferences[DISPLAY_TEXT_KEY] ?: "Roll to start!"
}
suspend fun saveDisplayText(text: String) {
context.dataStore.edit { preferences ->
preferences[DISPLAY_TEXT_KEY] = text
}
}
val getDieOne: Flow<String?> = context.dataStore.data
.map { preferences ->
preferences[DIE_ONE] ?: "1"
}
suspend fun saveDieOne(dieOne: Int) {
context.dataStore.edit { preferences ->
preferences[DIE_ONE] = dieOne.toString()
}
}
val getDieTwo: Flow<String?> = context.dataStore.data
.map { preferences ->
preferences[DIE_TWO] ?: "2"
}
suspend fun saveDieTwo(dieTwo: Int) {
context.dataStore.edit { preferences ->
preferences[DIE_TWO] = dieTwo.toString()
}
}
suspend fun resetDataStore() {
context.dataStore.edit { preferences ->
preferences.clear()
}
}
}
It is being called from a composable screen.
LaunchedEffect(true) {
sharedViewModel.setRoles()
sharedViewModel.saveChanges()
sharedViewModel.setupDataStore(context)
}
I was expecting (1) to work. It should run all of them at the same time and return the results accordingly. Instead of populating all of the values, it only populates the first one called. (3), works but I want to understand why it works and not (1) and (2).
Calling collect on a Flow suspends the coroutine until the Flow completes, but a Flow from DataStore never completes because it monitors for changes forever. So your first call to collect prevents the other code in your coroutine from ever being reached.
I'm not exactly why your second and third attempts aren't working, but they are extremely hacky anyway, delaying and cancelling as a means to avoid collecting forever.
Before continuing, I think you should remove the nullability from your Flow types:
val getDieOne: Flow<String?>
should be
val getDieOne: Flow<String>
since you are mapping to a non-nullable value anyway.
I don't know exactly what you're attempting, but I guess it is some initial setup in which you don't need to repeatedly update from the changing values in the Flows, so you only need the first value of each flow. You can use the first value to get that. Since these are pulling from the same data store, there's not really any reason to try to do it in parallel. So your function is pretty simple:
suspend fun setupDataStore(context: Context) {
with(TempDataStore(context)) {
die1.value = getDieOne.first().toInt()
displayText.value = getDisplayText.first()
die2.value = getDieTwo.first().toInt()
}
}
If you want just the first value, why not using the .first() method of Flow? And then you shouldn't need those new scopes!
Try out something like this:
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
die1.value = tempDataStore.getDieOne.first().toInt()
displayText.value = tempDataStore.getDisplayText.first()
die2.value = tempDataStore.getDieTwo.first().toInt()
}
EDIT:
Thanks Tenfour04 for the comment! You're right. I've fixed my code.
I'm creating a Pokémon app with Jetpack Compose. I'm testing it with two smartphones: Xioami Mi 11T Pro (Android 12) and Xiaomi Mi 8 Lite (Android 10).
Well, when I launch the app in the Mi 8 Lite, it starts correctly, the pokemon list loads perfectly.
But when I launch the app with the Mi 11 T Pro, it doesn't load, nothing shows. I discovered two things:
If I open the Layout Inspector it loads inmediately, without doing anything more...
When the screen is empty (just after launch, before it loads), If I click 1-2 times on the screen it starts to send the request and loads correctly.
Why is this happening?
I attach my ViewModel and my MainActivity.
PokemonListViewModel.kt
#HiltViewModel
class PokemonListViewModel #Inject constructor(
private val repository: PokemonRepositoryImpl
) : ViewModel() {
private var currentPage = 0
var pokemonList = mutableStateOf<List<PokedexListEntry>>(listOf())
var loadError = mutableStateOf("")
var isLoading = mutableStateOf(false)
var endReached = mutableStateOf(false)
private var cachedPokemonList = listOf<PokedexListEntry>()
private var isSearchStarting = true
var isSearching = mutableStateOf(false)
init {
loadPokemonList()
}
// TODO: Search online, not only already loaded pokémon
fun searchPokemonList(query: String) {
val listToSearch = if (isSearchStarting) {
pokemonList.value
} else {
// If we typed at least one character
cachedPokemonList
}
viewModelScope.launch(Dispatchers.Default) {
if (query.isEmpty()) {
pokemonList.value = cachedPokemonList
isSearching.value = false
isSearchStarting = true
return#launch
}
val results = listToSearch.filter {
// Search by name or pokédex number
it.pokemonName.contains(query.trim(), true) ||
it.number.toString() == query.trim()
}
if (isSearchStarting) {
cachedPokemonList = pokemonList.value
isSearchStarting = false
}
// Update entries with the results
pokemonList.value = results
isSearching.value = true
}
}
fun loadPokemonList() {
viewModelScope.launch {
isLoading.value = true
val result = repository.getPokemonList(PAGE_SIZE, currentPage * PAGE_SIZE)
when (result) {
is Resource.Success -> {
endReached.value = currentPage * PAGE_SIZE >= result.data!!.count
val pokedexEntries = result.data.results.mapIndexed { index, entry ->
val number = getPokedexNumber(entry)
val url = getImageUrl(number)
PokedexListEntry(
entry.name.replaceFirstChar(Char::titlecase),
url,
number.toInt()
)
}
currentPage++
loadError.value = ""
isLoading.value = false
pokemonList.value += pokedexEntries
}
is Resource.Error -> {
loadError.value = result.message!!
isLoading.value = false
}
is Resource.Loading -> {
isLoading.value = true
}
}
}
}
private fun getImageUrl(number: String): String {
return "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${number}.png"
}
private fun getPokedexNumber(entry: Result): String {
return if (entry.url.endsWith("/")) {
entry.url.dropLast(1).takeLastWhile { it.isDigit() }
} else {
entry.url.takeLastWhile { it.isDigit() }
}
}
}
MainActivity.kt
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val argPokemonName = "pokemonName"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposePokedexTheme {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "pokemon_list_screen") {
composable("pokemon_list_screen") {
PokemonListScreen(navController = navController)
}
composable(
"pokemon_detail_screen/{$argPokemonName}",
arguments = listOf(
navArgument(argPokemonName) {
type = NavType.StringType
}
)
) {
val pokemonName = remember {
it.arguments?.getString(argPokemonName)
}
PokemonDetailScreen(
pokemonName = pokemonName?.lowercase(Locale.ROOT) ?: "",
navController = navController
)
}
}
}
}
}
}
If someone knows why it doesn't load... I suspect that maybe init { } or Hilt injection are doing something that makes init doesn't start or something.
Thanks for your time and help!
Well, it seems is a Xiaomi reported Bug that google won't fix, you can see it here:
https://issuetracker.google.com/issues/227926002
It worked for me adding a little delay before set content and it seems to be working:
lifecycleScope.launch {
delay(300)
setContent {
JetpackComposePokedexTheme {
...
}
}
}
Also you can see: compose NavHost Start the white Screen
I tried to show a loading spinner, but loading state is always showing a false value on a compose function.
I've created a custom spinner, but it not shows
#Composable
private fun MainContent(viewModel: SearchJourneyViewModel = hiltViewModel()) {
val state = viewModel.state
Loader(isDialogVisible = state.isLoading)
}
In viewModel loading state is refreshing and returning a value that I need:
#HiltViewModel
class SearchJourneyViewModel #Inject constructor(
private val cityRepository: CityListRepository,
) : ViewModel() {
var state by mutableStateOf(SearchJourneyState().mock())
private set
init {
loadCityList()
}
private fun loadCityList() {
viewModelScope.launch {
cityRepository
.getCityList()
.collect { result ->
when (result) {
is Resource.Success -> {
state =
state.copy(
fromCity = //result,
toCity = //result,
isLoading = false,
error = null
)
}
}
is Resource.Error -> {
state =
state.copy(
fromCity = null,
toCity = null,
isLoading = false,
error = result.message
)
}
is Resource.Loading -> {
state =
state.copy(isLoading = result.isLoading)
}
}
}
}
}
}
And here is my state:
data class SearchJourneyState(
val cityList: List<City>? = null,
val isLoading: Boolean = false,
val isCityLoading: Boolean = false,
)
The main issue seems to be the way you are collecting your state.
Try define it as a StateFlow:
val viewStateFlow: StateFlow<VS> = MutableStateFlow()
And then simply collect it your UI layer:
val viewState by viewModel.viewStateFlow.collectAsState()
Then you should be able loading changes in UI :)
I have 5 api call function in the viewModel that I want to called parallel how can I do this? I put each of the function in the WithContext(Dispachers.IO) but it's not working. I used coroutines flow for calling api.
Note: I used clean architecture pattern and I have single use-case
ViewModel codes:
class MyJobsViewModel constructor(
private val myJobsUseCases: MyJobsUseCases,
private val clientNavigator: ClientNavigator
) : ViewModel(), ClientNavigator by clientNavigator {
private val _state = mutableStateOf(MyJobsState())
val state: State<MyJobsState> get() = _state
private fun getAllJobs(
offset: Int = 0,
limit: Int = 10,
type: JobTypeEnum = JobTypeEnum.ALL
) {
myJobsUseCases.getJobsUseCase.invoke(offset = offset, limit = limit, type = type)
.onEach {
when (it) {
is Resource.Success -> _state.value =
state.value.copy(
isLoading = false,
allJobItems = it.data ?: JobItemsResponse()
)
is Resource.Error -> _state.value =
state.value.copy(
isLoading = false,
error = it.message ?: "An unexpected error occurred"
)
is Resource.Loading -> _state.value = state.value.copy(isLoading = true)
}
}.launchIn(viewModelScope)
}
private fun getActiveJobs(
offset: Int = 0,
limit: Int = 10,
type: JobTypeEnum = JobTypeEnum.ALL
) {
myJobsUseCases.getJobsUseCase.invoke(offset = offset, limit = limit, type = type)
.onEach {
when (it) {
is Resource.Success -> _state.value =
state.value.copy(
isLoading = false,
activeJobItems = it.data ?: JobItemsResponse()
)
is Resource.Error -> _state.value =
state.value.copy(
isLoading = false,
error = it.message ?: "An unexpected error occurred"
)
is Resource.Loading -> _state.value = state.value.copy(isLoading = true)
}
}.launchIn(viewModelScope)
}
}
The best way to parallel multiple calls is to follow structured concurrency principle.
For example, when you need to evaluate the result of 5 independent network requests:
suspend fun fetchA(): Int { /* ... */ }
suspend fun fetchB(): Int { /* ... */ }
suspend fun fetchC(): Int { /* ... */ }
suspend fun fetchD(): Int { /* ... */ }
suspend fun fetchE(): Int { /* ... */ }
suspend fun overallResult(): Int = coroutineScope {
val a = async { fetchA() }
val b = async { fetchB() }
val c = async { fetchC() }
val d = async { fetchD() }
val e = async { fetchE() }
a.await() + b.await() + c.await() + d.await() + e.await()
}
Or when you need to make 5 independent api calls without returning any value:
suspend fun callA() { /* ... */ }
suspend fun callB() { /* ... */ }
suspend fun callC() { /* ... */ }
suspend fun callD() { /* ... */ }
suspend fun callE() { /* ... */ }
suspend fun makeCalls(): Unit = coroutineScope {
launch { callA() }
launch { callB() }
launch { callC() }
launch { callD() }
launch { callE() }
}
Wrapping function in launch or async block produces a new coroutine and executes in parallel.
coroutineScope organizes the area where launch and async can be used. It completes only when every child coroutine is completed.
I have a problem for now in JetpackCompose.
The problem is, when I'm collecting the Data from a flow, the value is getting fetched from firebase like there is a listener and the data's changing everytime. But tthat's not that.
I don't know what is the real problem!
FirebaseSrcNav
suspend fun getName(uid: String): Flow<Resource.Success<Any?>> = flow {
val query = userCollection.document(uid)
val snapshot = query.get().await().get("username")
emit(Resource.success(snapshot))
}
NavRepository
suspend fun getName(uid: String) = firebase.getName(uid)
HomeViewModel
fun getName(uid: String): MutableStateFlow<Any?> {
val name = MutableStateFlow<Any?>(null)
viewModelScope.launch {
navRepository.getName(uid).collect { nameState ->
when (nameState) {
is Resource.Success -> {
name.value = nameState.data
//_posts.value = state.data
loading.value = false
}
is Resource.Failure<*> -> {
Log.e(nameState.throwable, nameState.throwable)
}
}
}
}
return name
}
The probleme is in HomeScreen I think, when I'm calling the collectasState().value.
HomeScreen
val state = rememberLazyListState()
LazyColumn(
state = state,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(post) { post ->
//val difference = homeViewModel.getDateTime(homeViewModel.getTimestamp())
val date = homeViewModel.getDateTime(post.timeStamp!!)
val name = homeViewModel.getName(post.postAuthor_id.toString()).collectAsState().value
QuestionCard(
name = name.toString(),
date = date!!,
image = "",
text = post.postText!!,
like = 0,
response = 0,
topic = post.topic!!
)
}
}
I can't post video but if you need an image, imagine a textField where the test is alternating between "null" and "MyName" every 0.005 second.
Check official documentation.
https://developer.android.com/kotlin/flow
Flow is asynchronous
On viewModel
private val _name = MutableStateFlow<String>("")
val name: StateFlow<String>
get() = _name
fun getName(uid: String) {
viewModelScope.launch {
//asyn call
navRepository.getName(uid).collect { nameState ->
when (nameState) {
is Resource.Success -> {
name.value = nameState.data
}
is Resource.Failure<*> -> {
//manager error
Log.e(nameState.throwable, nameState.throwable)
}
}
}
}
}
on your view
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
viewModel.name.collect { name -> handlename
}
}
}