Problem with state in jetpackCompose and Flow - android

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
}
}
}

Related

Compose app doesn't load api data after launch

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

How to observe a MutableStateFlow list in Jetpack Compose

I have to implement Google's "Place Autocomplete" on Jetpack Compose, but the problem is that once I get the list of places, I can't update the UI.
Going into more detail, the places received from the google API are stored in a MutableStateFlow <MutableList <String>> and the status is observed in a Composable function via: databaseViewModel.autocompletePlaces.collectAsState(). However, when a new item is added to the list, the Composable function is not re-compiled
Class that gets the places:
class AutocompleteRepository(private val placesClient: PlacesClient)
{
val autocompletePlaces = MutableStateFlow<MutableList<String>>(mutableListOf())
fun fetchPlaces(query: String) {
val token = AutocompleteSessionToken.newInstance()
val request = FindAutocompletePredictionsRequest.builder()
.setSessionToken(token)
.setCountry("IT")
.setQuery(query)
.build()
placesClient.findAutocompletePredictions(request).addOnSuccessListener {
response: FindAutocompletePredictionsResponse ->
autocompletePlaces.value.clear()
for (prediction in response.autocompletePredictions) {
autocompletePlaces.value.add(prediction.getPrimaryText(null).toString())
}
}
}
}
ViewModel:
class DatabaseViewModel(application: Application): AndroidViewModel(application) {
val autocompletePlaces: MutableStateFlow<MutableList<String>>
val autocompleteRepository: AutocompleteRepository
init {
Places.initialize(application, apiKey)
val placesClient = Places.createClient(application)
autocompleteRepository = AutocompleteRepository(placesClient)
autocompletePlaces = autocompleteRepository.autocompletePlaces
}
fun fetchPlaces(query: String)
{
autocompleteRepository.fetchPlaces(query)
}
}
Composable function:
#Composable
fun dropDownMenu(databaseViewModel: DatabaseViewModel) {
var placeList = databaseViewModel.autocompletePlaces.collectAsState()
//From here on I don't think it's important, but I'll put it in anyway:
var expanded by rememberSaveable { mutableStateOf(true) }
var placeName by rememberSaveable { mutableStateOf("") }
Column {
OutlinedTextField(value = placeName, onValueChange =
{ newText ->
placeName = newText
databaseViewModel.fetchPlaces(newText)
})
DropdownMenu(expanded = expanded,
onDismissRequest = { /*TODO*/ },
properties = PopupProperties(
focusable = false,
dismissOnBackPress = true,
dismissOnClickOutside = true)) {
placeList.value.forEach { item ->
DropdownMenuItem(text = { Text(text = item) },
onClick = {
placeName = item
expanded = false
})
}
}
}
}
EDIT:
solved by changing: MutableStateFlow<MutableList<String>>(mutableListOf()) to MutableStateFlow<List<String>>(listOf()), but I still can't understand what has changed, since the structure of the list was also changed in the previous code
class AutocompleteRepository(private val placesClient: PlacesClient)
{
val autocompletePlaces = MutableStateFlow<List<String>>(listOf()) //Changed
fun fetchPlaces(query: String) {
val token = AutocompleteSessionToken.newInstance()
val request = FindAutocompletePredictionsRequest.builder()
.setSessionToken(token)
.setCountry("IT")
.setQuery(query)
.build()
val temp = mutableListOf<String>()
placesClient.findAutocompletePredictions(request).addOnSuccessListener {
response: FindAutocompletePredictionsResponse ->
for (prediction in response.autocompletePredictions) {
temp.add(prediction.getPrimaryText(null).toString())
}
autocompletePlaces.value = temp //changed
}
}
}
In your old code you were just changing the MutableList instance inside the StateFlow by adding items to it. This does not trigger an update because it is still the same list (but with extra values in it).
With your new code you are changing the whole value of the StateFlow to a new list which triggers an update.
You can simplify your new code to something like:
autocompletePlaces.update {
it + response.autocompletePredictions.map { prediction ->
prediction.getPrimaryText(null).toString()
}
}

Jetpack compose CircularProgressIndicator lag on api fetch

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.
}
}

LazyColumn notify about modification of item

I have a composable function that looks something like this:
#Composable
fun listScreen(context: Context, owner: ViewModelStoreOwner) {
val repository = xRepository(getAppDatabase(context).xDao()
val listData by repository.readAllData.observeAsState(emptyList())
// repository.readAllData returns LiveData<List<xEntity>>
// listData is a List<xEntity>
LazyColumn(){
items(listData.size) {
Card {
Text(listData[it].name)
Text(listData[it].hoursLeft.toString())
Button(onClick = {updateInDatabase(owner, name = listData[it], hoursLeft = 12)}) {...}
}
}
}
}
fun updateInDatabase(owner: ViewModelStoreOwner, name: String, hoursLeft: Int) {
val xViewModel....
val newEntity = xEntity(name=name, hoursLeft = Int)
xViewModel.update(newEntity)
}
and as you propably can guess, the LazyColumn doesn't refresh after modification of entity, is there a way to update listData after every update of entity?
edit:
class xRepository(private val xDatabaseDao) {
val readAllData: LiveData<List<xEntity>> = xDatabaseDao.getallXinfo()
...
suspend fun updatePlant(x: xEntity) {
plantzDao.updateX(x)
}
}
interface xDatabaseDao {
#Query("SELECT * FROM xInfo ORDER BY id DESC")
fun getAllXInfo(): LiveData<List<xEntity>>
....
#Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun updateX(x: xEntity?)
}
modification of entity:
fun updatePlantInDatabase(owner: ViewModelStoreOwner, name: String, waterAtHour: Int, selectedDays: ArrayList<Int>) {
val xViewModel: xViewModel = ViewModelProvider(owner).get(xViewModel::class.java)
val new = xEntity(name = name, waterAtHour = waterAtHour, selectedDays = selectedDays)
xViewModel.updatePlant(new)
}
I use mutableStateOf to wrap fields that need to recomposed. Such as
class TestColumnEntity(
val id: String,
title: String = ""
){
var title: String by mutableStateOf(title)
}
View:
setContent {
val mData = mutableStateListOf(
TestColumnEntity("id_0").apply { title = "cnm"},
TestColumnEntity("id_1").apply { title = "cnm"},
TestColumnEntity("id_2").apply { title = "cnm"},
TestColumnEntity("id_3").apply { title = "cnm"},
TestColumnEntity("id_4").apply { title = "cnm"},
TestColumnEntity("id_5").apply { title = "cnm"},
)
Column {
Button(onClick = {
mData.add(TestColumnEntity("id_${Random.nextInt(100) + 6}").apply { title = "ccnm" })
}) {
Text(text = "add data")
}
Button(onClick = {
mData[1].title = "test_${Random.nextInt(100)}"
}) {
Text(text = "update data")
}
TestLazyColumn(data = mData, key = {index, item ->
item.id
}) {
Text(text = it.title)
}
}
}
It works in my testcase
If you want to update lazy column (say recompose in jetpack compose) so use side effects.
Put list getting function in side effect (Launch Effect or other side effects) when list is change side effect automatic recompose your function and show updated list.

Coroutine scope cancel

I know that there are a lot of posts "How to cancel Coroutines Scope" but I couldn't find the answer for my case.
I have an Array of objects that I want to send each of them to Server using Coroutines.
What I need is, if one of my requests returns error, canceling others.
Here is my code:
private fun sendDataToServer(function: () -> Unit) {
LiabilitiesWizardSessionManager.getLiabilityAddedDocuments().let { documents ->
if (documents.isEmpty().not()) {
CoroutineScope(Dispatchers.IO).launch {
documents.mapIndexed { index, docDetail ->
async {
val result = uploadFiles(docDetail)
}
}.map {
var result = it.await()
}
}
} else function.invoke()
}
}
Below is my uploadFiles() function:
private suspend fun uploadFiles(docDetail: DocDetail): ArchiveFileResponse? {
LiabilitiesWizardSessionManager.mCreateLiabilityModel.let { model ->
val file = File(docDetail.fullFilePath)
val crmCode = docDetail.docTypeCode
val desc = docDetail.docTypeDesc
val id = model.commitmentMember?.id
val idType = 1
val createArchiveFileModel = CreateArchiveFileModel(108, desc, id, idType).apply {
this.isLiability = true
this.adaSystem = 3
}
val result = mRepositoryControllerKotlin.uploadFile(file, createArchiveFileModel)
return when (result) {
is ResultWrapper.Success -> {
result.value
}
is ResultWrapper.GenericError -> {
null
}
is ResultWrapper.NetworkError -> {
null
}
}
}
}
I know, I'm missing something.

Categories

Resources