App slow after making a request inside another request - android

I am making a request with coroutines based on a user name, which returns a list of Object<Profile>, and with that list I am making another request with each object, and then switching and passing the info to another screen, but such process is making the app super slow and I would like to find a better way or a way to not making this process so slow. Here my code
Fragment from where I am starting the process and where the app is getting super slow
emptyHomeViewModel.getPlayersListByName(text)
emptyHomeViewModel.listOfPlayersByNameLiveData.observe(viewLifecycleOwner) { playersByName ->
emptyHomeViewModel.getPlayersProfileByName(playersByName)
emptyHomeViewModel.listOfProfilesByID.observe(viewLifecycleOwner) { profiles ->
if (profiles != null) {
val list: Array<Profile> = profiles.toTypedArray()
bundle = Bundle().apply {
putSerializable("user", list)
}
findNavController().navigate(
R.id.action_emptyHomeFragment_to_selectUserFragment,
bundle
)
}
}
}
ViewModel from where I am executing the coroutines and making the request to the API
fun getPlayersListByName(playerName: String) = viewModelScope.launch {
val playersList = getPlayersByPersonaNameUseCase.getPlayersByName(playerName)
if (playersList != null) {
_listOfPlayersByNameLiveData.postValue(playersList)
}
}
fun getPlayersProfileByName(playersByName: List<PlayerByPersonaNameItem>?) =
viewModelScope.launch {
var playersProfileList: ArrayList<Profile> = arrayListOf()
if (playersByName != null) {
for (player in playersByName) {
getPlayerByIDUseCase.getPlayerById(player.accountId)
?.let { playersProfileList.add(it) }
}
_listOfProfilesByID.postValue(playersProfileList)
}
}

You can actually load profiles in parallel, preventing loading them one after another, to decrease time of loading data:
fun getPlayersProfileByName(playersByName: List<PlayerByPersonaNameItem>?) =
viewModelScope.launch {
val playersProfileList: List<Profile> = playersByName?.map { player ->
async {
getPlayerByIDUseCase.getPlayerById(player.accountId)
}
}.awaitAll().filterNotNull()
_listOfProfilesByID.postValue(playersProfileList)
}
Also you can improve it a little bit by removing additional LiveData observer and calling getPlayersProfileByName right after you get playersList:
fun getPlayersListByName(playerName: String) = viewModelScope.launch {
val playersList = getPlayersByPersonaNameUseCase.getPlayersByName(playerName)
getPlayersProfileByName(playersList)
}

Related

StateFlow collect not firing for list type

#HiltViewModel
class HistoryViewModel #Inject constructor(private val firebaseRepository: FirebaseRepository) :
ViewModel() {
private val translateList: MutableList<Translate> = mutableListOf()
private val _translateListState: MutableStateFlow<List<Translate>> =
MutableStateFlow(translateList)
val translateListState = _translateListState.asStateFlow()
init {
listenToSnapshotData()
}
private suspend fun addItemToList(translate: Translate) {
Log.d("customTag", "item added adapter $translate")
translateList.add(translate)
_translateListState.emit(translateList)
}
private suspend fun removeItemFromList(translate: Translate) {
Log.d("customTag", "item removed adapter $translate")
val indexOfItem = translateList.indexOfFirst {
it.id == translate.id
}
if (indexOfItem != -1) {
translateList.removeAt(indexOfItem)
_translateListState.emit(translateList)
}
}
private suspend fun updateItemFromList(translate: Translate) {
Log.d("customTag", "item modified adapter $translate")
val indexOfItem = translateList.indexOfFirst {
it.id == translate.id
}
if (indexOfItem != -1) {
translateList[indexOfItem] = translate
_translateListState.emit(translateList)
}
}
private fun listenToSnapshotData() {
viewModelScope.launch {
firebaseRepository.translateListSnapshotListener().collect { querySnapshot ->
querySnapshot?.let {
for (document in it.documentChanges) {
val translateData = document.document.toObject(Translate::class.java)
when (document.type) {
DocumentChange.Type.ADDED -> {
addItemToList(translate = translateData)
}
DocumentChange.Type.MODIFIED
-> {
updateItemFromList(translate = translateData)
}
DocumentChange.Type.REMOVED
-> {
removeItemFromList(translate = translateData)
}
}
}
}
}
}
}
}
Here data comes properly in querySnapshot in listenToSnapshotData function. And post that it properly calls corresponding function to update the list.
But after this line _translateListState.emit(translateList) flow doesn't go to corresponding collectLatest
private fun observeSnapShotResponse() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
historyViewModel.translateListState.collectLatest {
Log.d("customTag", "calling submitList from fragment")
translateListAdapter.submitList(it)
}
}
}
}
calling submitList from fragment is called once at the start, but as & when data is modified in list viewmodel, callback doesn't come to collectLatest
This is from StateFlow documentation:
Values in state flow are conflated using Any.equals comparison in a similar way to distinctUntilChanged operator. It is used to conflate incoming updates to value in MutableStateFlow and to suppress emission of the values to collectors when new value is equal to the previously emitted one.
You are trying to emit the same instance of List all the time, which has no effect because of what is written in the docs. You will have to create new instance of the list every time.

No response from Api when collected StateFlow does not change kotlin

In my fragment I have a RecyclerView, which displays results from the query I enter in options menu. It is an API from which I receive TV-shows list.
The query needs string with a len of 3 at least. When it's 1 or 2 the adapter is cleared.
override fun onQueryTextChange(newText: String?): Boolean {
if (newText != null && newText.length > 2) {
if (!newText.isNullOrBlank() && newText.length > 2)
viewModel.searchMovies(newText)
}
else {
adapter.setMoviesList(emptyList())
}
return true
}
However, I encountered an issue after entering e.g. "cat" twice. I received a list of shows having cat in it. After removing query from optionmenu and taping it again the adapter was empty. And there was no same search. For me -> because the flow value didn't change.
In ViewModel I have:
private val _moviesStateFlow = MutableStateFlow<List<TvMazeShowResponse>>(emptyList())
val moviesStateFlow = _moviesStateFlow as StateFlow<List<TvMazeShowResponse>>
fun searchMovies(query: String) {
viewModelScope.launch {
val response = api.getApiResponse(query)
_moviesStateFlow.emit(response)
}
}
And this StateFlow I collect in fragment.
lifecycleScope.launch {
viewModel.moviesStateFlow.collect {
adapter.setMoviesList(it)
}
}
To fix the problem I added another function in VM
fun clearFlow() {
viewModelScope.launch {
_moviesStateFlow.emit(emptyList())
}
}
And now in the fragment in onQueryTextChange in else I added.
else {
adapter.setMoviesList(emptyList())
viewModel.clearFlow()
}
Now it works as expected. But is there a better way to achieve that?
To make your code less convoluted, avoid doing logic in your UI classes (Fragment/Activity/Adapter) and make your ViewModel provide the single source of truth.
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.searchMovies(newText.orEmpty())
return true
}
// In ViewModel
fun searchMovies(query: String) {
val trimmedQuery = query.trim()
viewModelScope.launch {
val response = if (trimmedQuery.length <= 2) emptyList() else api.getApiResponse(trimmedQuery)
_moviesStateFlow.emit(response)
}
}
To avoid running multiple obsolete queries if the user is typing quickly, I suggest cancelling previous searches when starting new ones.
private val searchJob? = null
fun searchMovies(query: String) {
val trimmedQuery = query.trim()
searchJob?.cancel()
searchJob = viewModelScope.launch {
val response = if (trimmedQuery.length <= 2) emptyList() else api.getApiResponse(trimmedQuery)
_moviesStateFlow.emit(response)
}
}

calling an api multiple times using WorkManager in android

I want to call an api multiple times using WorkManager.
where idsArrayList is a list of ids.
I send each id in the api as Path to get response and similarly for other ids.
I want the workManager to return success after it has called api for all ids.
But the problem is WorkManager only returns SUCCESS for one id from the list. This is the first time I'm using WorkManager and I tried starting work manager for every id too by iterating over idsList one by one and making instance of workManger for every id in the for loop. But I thought sending the idsList as data in the workmanager and then itering over ids from inside doWork() would be better, but it's not working like I want and I don't understand why. Here's my code:
class MyWorkManager(appContext: Context, workerParams: WorkerParameters):
Worker(appContext, workerParams) {
private lateinit var callGrabShifts: Call<ConfirmStatus>
override fun doWork(): Result {
val idsList = inputData.getStringArray("IDS_LIST")
val idsArrayList = idsList?.toCollection(ArrayList())
var response = ""
if (idsArrayList != null) {
try {
response = callConfirmShiftApi(idsArrayList)
if (response.contains("CONFIRM")) {
return Result.success()
}
} catch (e: Exception) {
e.printStackTrace()
return Result.failure()
}
}
return Result.retry()
}
private fun callConfirmShiftApi(idsArrayList: ArrayList<String>): String {
var response = ""
for ((index, id) in idsArrayList.withIndex()) {
response = callApiForId(id)
if(index == idsArrayList.lastIndex) {
response = "CONFIRM"
}
}
return response
}
private fun callApiForId(id: String): String {
var shiftGrabStatus = ""
callGrabShifts = BaseApp.apiInterface.confirmGrabAllShifts(BaseApp.userId, id)
callGrabShifts.enqueue(object : Callback<ConfirmStatus> {
override fun onResponse(call: Call<ConfirmStatus>, response: Response<ConfirmStatus>) {
if (response.body() != null) {
shiftGrabStatus = response.body()!!.status
if (shiftGrabStatus != null) {
if (shiftGrabStatus.contains("CONFIRM")) {
val shiftNumber = ++BaseApp.noOfShiftsGrabbed
sendNotification(applicationContext)
shiftGrabStatus = "CONFIRM"
return
} else {
shiftGrabStatus = "NOT CONFIRM"
return
}
} else {
shiftGrabStatus = "NULL"
return
}
} else {
shiftGrabStatus = "NULL"
return
}
}
override fun onFailure(call: Call<ConfirmStatus>, t: Throwable) {
shiftGrabStatus = "FAILURE"
return
}
})
return shiftGrabStatus
}
}
And this is the code where I'm starting the WorkManager:
private fun confirmShiftApi(availableShiftsIdList: ArrayList<String>) {
val data = Data.Builder()
data.putStringArray("IDS_LIST", availableShiftsIdList.toArray(arrayOfNulls<String>(availableShiftsIdList.size)))
val oneTimeWorkRequest = OneTimeWorkRequestBuilder<MyWorkManager>().setInputData(data.build())
.build()
WorkManager.getInstance(applicationContext).enqueue(oneTimeWorkRequest)
WorkManager.getInstance(this).getWorkInfoByIdLiveData(oneTimeWorkRequest.id)
.observe(this, Observer { workInfo: WorkInfo? ->
if (workInfo != null && workInfo.state.isFinished) {
val progress = workInfo.progress
}
Log.d("TESTING", "(MainActivity) : observing work manager - workInfo?.state - ${workInfo?.state}")
})
}
Any suggestions what I might be doing wrong or any other alternative to perform the same? I chose workmanager basicaly to perform this task even when app is closed and for learning purposes as I haven't used WorkManager before. But would switch to other options if this doesn't work.
I tried the following things:
removed the 'var response line in every method that I'm using to set the response, though I added it temporarily just for debugging earlier but it was causing an issue.
I removed the check for "CONFIRM" in doWork() method and just made the api calls, removed the extra return lines.
I tried adding manual delay in between api calls for each id.
I removed the code where I'm sending the ids data from my activity before calling workmanager and made the api call to fetch those ids inside workmanager and added more delay in between those calls to that keep running in background to check for data one round completes(to call api for all ids that were fetched earlier, it had to call api again to check for more ids on repeat)
I removed the extra api calls from onRestart() and from other conditons that were required to call api again.
I tested only one round of api calls for all ids with delay and removed the repeated call part just to test first. Didn't work.
None of the above worked, it just removed extra lines of code.
This is my final code that is tested and It cleared my doubt. Though it didn't fix this issue as the problem was because of backend server and Apis were returning failure in onResponse callback for most ids(when calls are made repeatedly using a for loop for each id) except first id and randomly last id from the list sometimes(with delay) for the rest of the ids it didn't return CONFIRM status message from api using Workmanager. Adding delay didn't make much difference.
Here's my Workmanager code:
class MyWorkManager(appContext: Context, workerParams: WorkerParameters):
Worker(appContext, workerParams) {
private lateinit var callGrabShifts: Call<ConfirmStatus>
override fun doWork(): Result {
val idsList = inputData.getStringArray("IDS_LIST")
val idsArrayList = idsList?.toCollection(ArrayList())
if (idsArrayList != null) {
try {
response = callConfirmShiftApi(idsArrayList)
if (response.contains("CONFIRM")) {
return Result.success()
}
} catch (e: Exception) {
e.printStackTrace()
return Result.failure()
}
}
return Result.success()
}
private fun callConfirmShiftApi(idsArrayList: ArrayList<String>): String {
for ((index, id) in idsArrayList.withIndex()) {
response = callApiForId(id)
Thread.sleep(800)
if(index == idsArrayList.lastIndex) {
response = "CONFIRM"
}
}
return response
}
private fun callApiForId(id: String): String {
callGrabShifts = BaseApp.apiInterface.confirmGrabAllShifts(BaseApp.userId, id)
callGrabShifts.enqueue(object : Callback<ConfirmStatus> {
override fun onResponse(call: Call<ConfirmStatus>, response: Response<ConfirmStatus>) {
if (response.body() != null) {
shiftGrabStatus = response.body()!!.status
if (shiftGrabStatus != null) {
if (shiftGrabStatus.contains("CONFIRM")) {
return
} else {
return
}
} else {
return
}
} else {
return
}
}
override fun onFailure(call: Call<ConfirmStatus>, t: Throwable) {
return
}
})
return shiftGrabStatus
}
Eventually this problem(when an individual call is made for an id, it always returns success but when i call the api for every id using a loop, it only returns success for first call and failure for others) was solved using Service, it didn't have a complete success rate from apis either, but for 6/11 ids the api returned success(400ms delay between each api call), so it served the purpose for now.

Android Coroutines - How can I wait for two different retrofit calls and wrap both results into one observable?

In my view, I need to wait for two different data sets to populate a recycler view.
This is my completely non working function in ViewModel, here it shows what I'm trying to achieve.
val resultWrapperLiveData = MutableLiveData<SearchResultWrapper>()
fun searchAandB(query: String) {
var a: AObject? = null
var b: BObject? = null
CoroutineScope(Dispatchers.Main).launch {
launch {
a = repo.searchA(query = query)
}
launch {
b = repo.searchB(query = query)
}
resultWrapperLiveData.postValue(SearchResultWrapper(a, b))
}
}
Thanks in advance!
I think this is a solution. The only problem with this is it waits to start the second api call. It would be better if I can get both of these running at the same time.
fun searchAandB(query: String) {
CoroutineScope(Dispatchers.Main).launch {
val a = CoroutineScope(Dispatchers.IO).async rt#{
return#rt repo.searchA(query = query)
}.await()
val b = CoroutineScope(Dispatchers.IO).async rt#{
return#rt repo.searchB(query = query)
}.await()
_searchResultWrapper.postValue(SearchResultWrapper(a, b))
}
}

Firebase realtime snapshot listener using Coroutines

I want to be able to listen to realtime updates in Firebase DB's using Kotlin coroutines in my ViewModel.
The problem is that whenever a new message is created in the collection my application freezes and won't recover from this state. I need to kill it and restart app.
For the first time it passes and I can see the previous messages on the UI. This problem happens when SnapshotListener is called for 2nd time.
My observer() function
val channel = Channel<List<MessageEntity>>()
firestore.collection(path).addSnapshotListener { data, error ->
if (error != null) {
channel.close(error)
} else {
if (data != null) {
val messages = data.toObjects(MessageEntity::class.java)
//till this point it gets executed^^^^
channel.sendBlocking(messages)
} else {
channel.close(CancellationException("No data received"))
}
}
}
return channel
That's how I want to observe messages
launch(Dispatchers.IO) {
val newMessages =
messageRepository
.observer()
.receive()
}
}
After I replacing sendBlocking() with send() I am still not getting any new messages in the channel. SnapshotListener side is executed
//channel.sendBlocking(messages) was replaced by code bellow
scope.launch(Dispatchers.IO) {
channel.send(messages)
}
//scope is my viewModel
How to observe messages in firestore/realtime-dbs using Kotlin coroutines?
I have these extension functions, so I can simply get back results from the query as a Flow.
Flow is a Kotlin coroutine construct perfect for this purposes.
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/
#ExperimentalCoroutinesApi
fun CollectionReference.getQuerySnapshotFlow(): Flow<QuerySnapshot?> {
return callbackFlow {
val listenerRegistration =
addSnapshotListener { querySnapshot, firebaseFirestoreException ->
if (firebaseFirestoreException != null) {
cancel(
message = "error fetching collection data at path - $path",
cause = firebaseFirestoreException
)
return#addSnapshotListener
}
offer(querySnapshot)
}
awaitClose {
Timber.d("cancelling the listener on collection at path - $path")
listenerRegistration.remove()
}
}
}
#ExperimentalCoroutinesApi
fun <T> CollectionReference.getDataFlow(mapper: (QuerySnapshot?) -> T): Flow<T> {
return getQuerySnapshotFlow()
.map {
return#map mapper(it)
}
}
The following is an example of how to use the above functions.
#ExperimentalCoroutinesApi
fun getShoppingListItemsFlow(): Flow<List<ShoppingListItem>> {
return FirebaseFirestore.getInstance()
.collection("$COLLECTION_SHOPPING_LIST")
.getDataFlow { querySnapshot ->
querySnapshot?.documents?.map {
getShoppingListItemFromSnapshot(it)
} ?: listOf()
}
}
// Parses the document snapshot to the desired object
fun getShoppingListItemFromSnapshot(documentSnapshot: DocumentSnapshot) : ShoppingListItem {
return documentSnapshot.toObject(ShoppingListItem::class.java)!!
}
And in your ViewModel class, (or your Fragment) make sure you call this from the right scope, so the listener gets removed appropriately when the user moves away from the screen.
viewModelScope.launch {
getShoppingListItemsFlow().collect{
// Show on the view.
}
}
What I ended up with is I used Flow which is part of coroutines 1.2.0-alpha-2
return flowViaChannel { channel ->
firestore.collection(path).addSnapshotListener { data, error ->
if (error != null) {
channel.close(error)
} else {
if (data != null) {
val messages = data.toObjects(MessageEntity::class.java)
channel.sendBlocking(messages)
} else {
channel.close(CancellationException("No data received"))
}
}
}
channel.invokeOnClose {
it?.printStackTrace()
}
}
And that's how I observe it in my ViewModel
launch {
messageRepository.observe().collect {
//process
}
}
more on topic https://medium.com/#elizarov/cold-flows-hot-channels-d74769805f9
Extension function to remove callbacks
For Firebase's Firestore database there are two types of calls.
One time requests - addOnCompleteListener
Realtime updates - addSnapshotListener
One time requests
For one time requests there is an await extension function provided by the library org.jetbrains.kotlinx:kotlinx-coroutines-play-services:X.X.X. The function returns results from addOnCompleteListener.
For the latest version, see the Maven Repository, kotlinx-coroutines-play-services.
Resources
Using Firebase on Android with Kotlin Coroutines by Joe Birch
Using Kotlin Extension Functions and Coroutines with Firebase by Rosário Pereira Fernandes
Realtime updates
The extension function awaitRealtime has checks including verifying the state of the continuation in order to see whether it is in isActive state. This is important because the function is called when the user's main feed of content is updated either by a lifecycle event, refreshing the feed manually, or removing content from their feed. Without this check there will be a crash.
ExtenstionFuction.kt
data class QueryResponse(val packet: QuerySnapshot?, val error: FirebaseFirestoreException?)
suspend fun Query.awaitRealtime() = suspendCancellableCoroutine<QueryResponse> { continuation ->
addSnapshotListener({ value, error ->
if (error == null && continuation.isActive)
continuation.resume(QueryResponse(value, null))
else if (error != null && continuation.isActive)
continuation.resume(QueryResponse(null, error))
})
}
In order to handle errors the try/catch pattern is used.
Repository.kt
object ContentRepository {
fun getMainFeedList(isRealtime: Boolean, timeframe: Timestamp) = flow<Lce<PagedListResult>> {
emit(Loading())
val labeledSet = HashSet<String>()
val user = usersDocument.collection(getInstance().currentUser!!.uid)
syncLabeledContent(user, timeframe, labeledSet, SAVE_COLLECTION, this)
getLoggedInNonRealtimeContent(timeframe, labeledSet, this)
}
// Realtime updates with 'awaitRealtime' used
private suspend fun syncLabeledContent(user: CollectionReference, timeframe: Timestamp,
labeledSet: HashSet<String>, collection: String,
lce: FlowCollector<Lce<PagedListResult>>) {
val response = user.document(COLLECTIONS_DOCUMENT)
.collection(collection)
.orderBy(TIMESTAMP, DESCENDING)
.whereGreaterThanOrEqualTo(TIMESTAMP, timeframe)
.awaitRealtime()
if (response.error == null) {
val contentList = response.packet?.documentChanges?.map { doc ->
doc.document.toObject(Content::class.java).also { content ->
labeledSet.add(content.id)
}
}
database.contentDao().insertContentList(contentList)
} else lce.emit(Error(PagedListResult(null,
"Error retrieving user save_collection: ${response.error?.localizedMessage}")))
}
// One time updates with 'await' used
private suspend fun getLoggedInNonRealtimeContent(timeframe: Timestamp,
labeledSet: HashSet<String>,
lce: FlowCollector<Lce<PagedListResult>>) =
try {
database.contentDao().insertContentList(
contentEnCollection.orderBy(TIMESTAMP, DESCENDING)
.whereGreaterThanOrEqualTo(TIMESTAMP, timeframe).get().await()
.documentChanges
?.map { change -> change.document.toObject(Content::class.java) }
?.filter { content -> !labeledSet.contains(content.id) })
lce.emit(Lce.Content(PagedListResult(queryMainContentList(timeframe), "")))
} catch (error: FirebaseFirestoreException) {
lce.emit(Error(PagedListResult(
null,
CONTENT_LOGGED_IN_NON_REALTIME_ERROR + "${error.localizedMessage}")))
}
}
This is working for me:
suspend fun DocumentReference.observe(block: suspend (getNextSnapshot: suspend ()->DocumentSnapshot?)->Unit) {
val channel = Channel<Pair<DocumentSnapshot?, FirebaseFirestoreException?>>(Channel.UNLIMITED)
val listenerRegistration = this.addSnapshotListener { value, error ->
channel.sendBlocking(Pair(value, error))
}
try {
block {
val (value, error) = channel.receive()
if (error != null) {
throw error
}
value
}
}
finally {
channel.close()
listenerRegistration.remove()
}
}
Then you can use it like:
docRef.observe { getNextSnapshot ->
while (true) {
val value = getNextSnapshot() ?: continue
// do whatever you like with the database snapshot
}
}
If the observer block throws an error, or the block finishes, or your coroutine is cancelled, the listener is removed automatically.

Categories

Resources