I have written a code to fetch data from Cloud Firestore and am trying to implement the network calls using coroutines. I have tried to follow the official guides as much as possible, but since the functions have been left incomplete in those docs, I have made adjustments according to my requirements, but those might be the problem itself.
Here's the function which fetches the data:
suspend fun fetchHubList(): MutableLiveData<ArrayList<HubModel>> = withContext(Dispatchers.IO) {
val hubList = ArrayList<HubModel>()
val liveHubData = MutableLiveData<ArrayList<HubModel>>()
hubsListCollection.get().addOnSuccessListener { collection ->
if (collection != null) {
Log.d(TAG, "Data fetch successful!")
for (document in collection) {
Log.d(TAG, "the document id is ")
hubList.add(document.toObject(HubModel::class.java))
}
} else {
Log.d(TAG, "No such document")
}
}.addOnFailureListener { exception ->
Log.d(TAG, "get failed with ", exception)
}
if (hubList.isEmpty()) {
Log.d(TAG, "Collection size 0")
} else {
Log.d(TAG, "Collection size not 0")
}
liveHubData.postValue(hubList)
return#withContext liveHubData
}
And here is the ViewModel class which is calling this method:
class HubListViewModel(application: Application): AndroidViewModel(application) {
// The data which will be observed
var hubList = MutableLiveData<ArrayList<HubModel>>()
private val hubListDao = HubListDao()
init {
viewModelScope.launch (Dispatchers.IO){
hubList = hubListDao.fetchHubList()
Log.d(TAG, "Array List fetched")
}
}
}
Using the tag messages I know that an empty list is being returned, which I know from another question of mine, is because the returned ArrayList is not in sync with the fetching operation, but I don't know why, since I've wrapped the whole function inside a with context block. Please tell me why the return and fetching is not being performed sequentially.
you should add this dependency "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.3". It allows you to use await() to replace callbacks.
suspend fun fetchHubList(): List<HubModel>? = try {
hubsListCollection.get().await().map { document ->
Log.d(TAG, "the document id is ${document.id}")
document.toObject(HubModel::class.java)
}.apply {
Log.d(TAG, "Data fetch successful!")
Log.d(TAG, "Collection size is $size")
}
} catch (e: Exception) {
Log.d(TAG, "get failed with ", e)
null
}
Dispatchers.IO is not necessary since firebase APIs are main-safe
class HubListViewModel(application: Application): AndroidViewModel(application) {
val hubList = MutableLiveData<List<HubModel>>()
private val hubListDao = HubListDao()
init {
viewModelScope.launch {
hubList.value = hubListDao.fetchHubList()
Log.d(TAG, "List fetched")
}
}
}
Related
I am using the Firestore database and need to fetch some data from it. How do I make the function await for the data before returning the list?
fun getExercise(bodyPart: String): MutableList<String> {
val db = Firebase.firestore
val exercisesList = mutableListOf<String>()
val exercise = db.collection("exercises")
val query = exercise.whereEqualTo("body-part", bodyPart)
query.get().addOnSuccessListener { result ->
for(temp in result) {
exercisesList.add(temp.id)
Log.d(TAG, "${temp.id} => ${temp.data}")
}
}
.addOnFailureListener { exception ->
Log.w(TAG, "Error getting documents: ", exception)
}
return exercisesList
}
I know I need to use .await() but I am new to Kotlin and can't make it work.
I see 2 possible options:
1. Change your code in order to call another function with the result instead of returning the result
fun getExercise(bodyPart: String) {
val db = Firebase.firestore
val exercisesList = mutableListOf<String>()
val exercise = db.collection("exercises")
val query = exercise.whereEqualTo("body-part", bodyPart)
query.get().addOnSuccessListener { result ->
for(temp in result) {
exercisesList.add(temp.id)
Log.d(TAG, "${temp.id} => ${temp.data}")
}
// Call another function with the result:
anotherFunction(exercisesList)
}.addOnFailureListener { exception ->
Log.w(TAG, "Error getting documents: ", exception)
}
}
2. Implement Kotlin Coroutines
This option might be a little more complex than the first one for someone who's new to the language, as you'd need to understand the concept of Kotlin Coroutines.
Start by adding the Coroutines dependency to your build.gradle file:
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}
Change your function to become a suspend function:
suspend fun getExercise(bodyPart: String): MutableList<String> {
// ...
}
Use the await() extension function to fetch the result:
suspend fun getExercise(bodyPart: String): MutableList<String> {
val db = Firebase.firestore
val exercisesList = mutableListOf<String>()
val exercise = db.collection("exercises")
val query = exercise.whereEqualTo("body-part", bodyPart)
try {
val result = query.get().await()
for (temp in result) {
exercisesList.add(temp.id)
Log.d(TAG, "${temp.id} => ${temp.data}")
}
return exercisesList
} catch (e: Exception) {
Log.w(TAG, "Error getting documents: ", exception)
return emptyList() // returning an empty list in case the fetch fails
}
}
Use a Coroutine scope when making a call to your suspend function:
// If you're calling from an Activity/Fragment, you can use the
// lifecycleScope from the lifecycle-runtime-ktx library
//
// If you're calling from a ViewModel, consider using the
// viewModelScope from the lifecycle-viewmodel-ktx library
//
// See https://d.android.com/topic/libraries/architecture/coroutines#dependencies
// for more details
lifecycleScope.launch { // Coroutine Scope
val exercises: MutableList<String> = getExercise("arms")
// use the list
}
More resources:
Coroutines on Android
How to use Kotlin Coroutines with Firebase APIs Youtube Short
This method is intended to get data from server and return them.
requestGroups() method in my code calls execute() method of third party library. execute() launches background executor that adds data to my list, but it returns empty list, because data adding to list occurs in the background and return happens immediately after executor lauching. How can I make return statement wait until all data are added to the list?
RepositoryImpl.kt:
class RepositoryImpl : Repository {
override fun requestGroups(): List<VKGroup> {
val vkGroups = mutableListOf<VKGroup>()
VK.execute(GroupsService().groupsGetExtended(), object : // execute launches executor...
VKApiCallback<GroupsGetObjectExtendedResponse> {
override fun success(result: GroupsGetObjectExtendedResponse) {
val groups = result.items
Log.d(TAG, "result groups in repo: ${groups}")
if (groups.isNotEmpty()) {
groups.forEach { group ->
vkGroups.add(
VKGroup(
id = group.id.value,
name = group.name ?: "",
photo = group.photo200 ?: ""
)
)
}
}
Log.d(TAG, "vkGroups in repo: ${vkGroups}")
}
override fun fail(error: Exception) {
Log.e(TAG, error.toString())
}
})
Log.d(TAG, "vkGroups in repo before return: ${vkGroups}")
return vkGroups //and immediately returns empty list
}
}
Third party execute function:
/**
* Execute api request with callback
* You can use this method in UI thread
* Also you can use your own async mechanism, like coroutines or RX
*/
#JvmStatic
fun <T>execute(request: ApiCommand<T>, callback: VKApiCallback<T>? = null) {
VKScheduler.networkExecutor.submit {
try {
val result = executeSync(request)
VKScheduler.runOnMainThread(Runnable {
callback?.success(result)
})
} catch (e: Exception) {
VKScheduler.runOnMainThread(Runnable {
if (e is VKApiExecutionException && e.isInvalidCredentialsError) {
handleTokenExpired()
}
callback?.fail(e)
})
}
}
}
I have a method that looks like that:
private lateinit var cards: List<Card>
fun start() = viewModelScope.launch {
if (!::cards.isInitialized) {
getCards().collect { result ->
result
.doIfSuccess {
cards = it.data
Log.d(TAG, "Received cards")
}
.doIfError {
_errorState.setIfNotEqual(it.exception)
Log.e(TAG, "Cards were not received because of ${it.exception}")
return#collect // <--- that's the place
}
}
}
Log.d(TAG, "Message that needs to be shown only if cards were received")
if (сards.isEmpty()) {
Log.e(TAG, "Сards list is empty")
_errorState.setIfNotEqual(NoCardsException)
return#launch
}
val сard = сards[0]
}
I need to completely return from the method, not only from the .collect block, I've tried to use return#launch or some other custom labels, but it doesn't work even though Kotlin compiler suggests me to set it like that:
I think you can use transformWhile to create a new Flow that does an operation on each item you receive until you return false. Then collect that Flow. I didn't test this because I'm not really sure of how you've structured .doIfSuccess and .doIfError.
fun start() = viewModelScope.launch {
if (!::cards.isInitialized) {
getCards().transformWhile { result ->
result
.doIfSuccess {
cards = it.data
Log.d(TAG, "Received cards")
}
.doIfError {
_errorState.setIfNotEqual(it.exception)
Log.e(TAG, "Cards were not received because of ${it.exception}")
return#transformWhile false
}
return#transformWhile true
}.collect()
}
//...
}
EDIT:
If you only want the first value from the Flow, you could do this:
fun start() = viewModelScope.launch {
if (!::cards.isInitialized) {
getCards().first()
.doIfSuccess {
cards = it.data
Log.d(TAG, "Received cards")
}
.doIfError {
_errorState.setIfNotEqual(it.exception)
Log.e(TAG, "Cards were not received because of ${it.exception}")
return#launch
}
}
//...
}
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.
I am retriving all documents from firestore and storing it in a ArrayList with a each element being a custom dataType. The data retriving process takes a little time and so I want to make the listView after the data has been retrived. I have used AsyncTask before and would have used the onPostExectue function but I came to know about doAsync in kotlin and wanted to gitve it a try.
Can someone guide me how to do it?
This is the function to get Data
fun initFirestore(): ArrayList<MetaData>{
FirebaseApp.initializeApp(this#MainActivity)
val db = FirebaseFirestore.getInstance()
val returnData: ArrayList<MetaData> = ArrayList()
db.collection("name_of_collection")
.orderBy("id")
.get()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
for (document in task.result) {
val data = document.data
returnData.add(MetaData(data["name"].toString(), data["artist"].toString(), data["url"].toString()))
// Log.d("Test", document.id + " => " + data)
}
} else {
// Log.d("Test", "Error getting documents: ", task.exception)
}
}
return returnData
}
The log shows the correct data. And I am calling it from another function
fun getSongs(){
doAsync {
val test = initFirestore()
onComplete {
Log.v("Test","$test")
}
uiThread {
Log.v("A","$test")
}
}
}
Both log here return empty list