I am creating an observable by calling Observable.create. I want to keep it alive as long as there are subscribers. In this case, setCancellable seems to me the only place to call onComplete(), or should it be called at all?
val data: Observable<Data>
get() = Observable.create { emitter ->
val ref = mDatabase.child("users").child("data")
val listener = ref.addValueEventListener(object : ValueEventListener {
override fun onCancelled(p0: DatabaseError) {
emitter.onError(p0.toException())
}
override fun onDataChange(p0: DataSnapshot) {
Data.parse(p0)?.let { emitter.onNext(it) }
}
})
emitter.setCancellable {
emitter.onComplete() // ???
ref.removeEventListener(listener)
}
}
Related
I have a specific UseCase where initialize app data. I store every <reference, listener> in a dispatchListeners list to unsubscribe later.
typealias EventListener = Pair<DatabaseReference, ValueEventListener>
class InitAppDataUseCase(
private val subscribeUserUseCase: SubscribeUserUseCase,
private val subscribeNewsUseCase: SubscribeNewsUseCase,
private val subscribeStoriesUseCase: SubscribeStoriesUseCase,
private val subscribeMeetingsUseCase: SubscribeMeetingsUseCase,
private val subscribeCategoriesUseCase: SubscribeCategoriesUseCase,
private val dispatchers: AppDispatchers
): UseCase<Unit, Unit> {
private val dispatchListeners = mutableListOf<EventListener>()
override suspend fun execute(input: Unit) {
init()
}
private fun EventListener.add() = dispatchListeners.add(this)
private suspend fun init() = CoroutineScope(dispatchers.io).launch {
runCatching {
listOf(
async { subscribeUserUseCase.execute().add() },
async { subscribeNewsUseCase.execute().add() },
async { subscribeStoriesUseCase.execute().add() },
async { subscribeMeetingsUseCase.execute().add() },
async { subscribeCategoriesUseCase.execute().add() }
).awaitAll()
}
}
fun clearSubscribed() = CoroutineScope(dispatchers.io).launch {
dispatchListeners.forEach { referenceToListener ->
with(referenceToListener) {
first.removeEventListener(second)
}
}
}
}
But where should I unsubscribe?
When the user remove an account or sign out from my app, I do this in specific ViewModel and redirect him to AuthScreen after this executed.
But what should I do if user just close my app? Is this correct way to unsubscribe in onDestroy() of my MainActivity? I have doubts because clearSubscribed() is a heavy operation. Am I right if the user have a poor internet connection and - this operation couldn't be executed because applicationScope will be dead?
class MainActivity : ComponentActivity() {
private val initAppDataUseCase by inject<InitAppDataUseCase>()
override fun onCreate() {}
override fun onDestroy() {
super.onDestroy()
initAppDataUseCase.clearSubscribed()
}
}
You have to remove the listener according to the life cycle of your activity. Since you're using Kotlin, most likely in an MVVM architecture, I would rather use callbackFlow. There is a very helpful example in the documentation. However, in the case of Firebase, to attach and dettach the listener, please use the following lines of code:
override fun getDataFromRealtimeDatabase() = callbackFlow {
val listener = object: ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
//Do what you need to do with the data.
}
override fun onCancelled(e: DatabaseError) {
Log.d("TAG", "${e?.message}") //Never ignore potential errors!
}
}
yourRef.addValueEventListener(listener) //Attach the listener.
awaitClose {
yourRef.removeEventListener(listener) //Dettach the listener.
}
}
I am building a chat app, and I am retrieving last chats with unread messages.
I am using firebase as my database and MVVM pattern.
So the problem is that I have to run 3 firebase requests in order to get HashMap with contact name and last unseen messages.
I am running the first function "getUsersChatList" that is located in the repository.
In that function I call the second function "getUsersLastChat(arrayList)" and I am passing the result of the last function.
From "getUsersLastChat(arrayList)" I call the last function "searchNumberOfMessages(user)" and I am passing the result of the previous function.
"searchNumberOfMessages" result is a List of HashMap with the contact name and the unread messages.
I declared at the top of my repository a MutableLiveData that stores the list of hashMap and I created a function to observe its changes,however,its not getting called because the value is not updated and I don't know why.
Here is what I have done:
Repository:
Top repository:
var newMessages = MutableLiveData<List<HashMap<String, Any?>>>()
getUsersChatList:
fun getUsersChatList(){
val arrayList = ArrayList<ChatList>()
chatListRef.child(currentUser!!.uid).addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
for (snapshot2 in snapshot.children) {
val singleListItem = snapshot2.getValue(ChatList::class.java)
if (singleListItem != null) {
arrayList.add(singleListItem)
}
}
getUsersLastChat(arrayList)
}
override fun onCancelled(error: DatabaseError) {
}
})
}
getUsersLastChat:
private fun getUsersLastChat(arrayList: ArrayList<ChatList>){
usersRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
for (snapshot2 in snapshot.children){
val user = snapshot2.getValue(User::class.java)
for (eachChatList in arrayList)
{
if (user!!.uid.equals(eachChatList.id))
{
searchNumberOfMessages(user)
}
}
}
}
override fun onCancelled(error: DatabaseError) {
TODO("Not yet implemented")
}
})
}
This is where the problem,searchNumberOfMessages:
fun searchNumberOfMessages(user: User) : MutableLiveData<List<HashMap<String, Any?>>> {
val messageArray = ArrayList<String?>()
val hashMapArray = ArrayList<HashMap<String, Any?>>()
val hasMap = HashMap<String, Any?>()
chatRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
for (snapshot2 in snapshot.children) {
val chat = snapshot2.getValue(Message::class.java)
if (chat!!.receiver.equals(currentUser!!.uid) && chat.sender.equals(
user.uid)) {
if (chat.seen == false){
messageArray.add(chat.message)
}
}
}
hasMap["Sender"] = user.userName
hasMap["Messages"] = messageArray.size
hashMapArray.add(hasMap)
newMessages.value = hashMapArray
Log.e("newMessages", "newMessages: ${newMessages.value}") // Getting the current Values with No Problem
}
override fun onCancelled(error: DatabaseError) {
TODO("Not yet implemented")
}
})
Log.e("newMessages2", "newMessages2: ${newMessages.value}") // Getting null on the same value and pass the null object to the viewModel
return newMessages
}
In the viewModel:
fun getUsersChatList(): MutableLiveData<List<HashMap<String, Any?>>> {
mainRepository.getUsersChatList()
return mainRepository.newMessages
}
And in the activity:
chatsListFragmentViewModel.getUsersChatList().observe(viewLifecycleOwner,object : Observer<List<HashMap<String, Any?>>?> {
override fun onChanged(t: List<HashMap<String, Any?>>?) {
Log.e("userMessagesHashMap","userMessagesHashMap ${t}")
}
})
This is the result in logcat:
2022-01-20 04:13:31.604 13145-13145/com.dapps.misronim E/newMessages2: newMessages2: null
2022-01-20 04:13:31.610 13145-13145/com.dapps.misronim E/newMessages2: newMessages2: null
2022-01-20 04:13:31.779 13145-16093/com.dapps.misronim D/FA: Application going to the background
2022-01-20 04:13:31.859 13145-13145/com.dapps.misronim E/newMessages: newMessages: [{Sender=Mario, Messages=4}]
2022-01-20 04:13:31.873 13145-13145/com.dapps.misronim E/newMessages: newMessages: [{Sender=Dj, Messages=2}]
2022-01-20 04:13:31.886 13145-13145/com.dapps.misronim E/newMessages: newMessages: [{Sender=Jfioo32856, Messages=0}]
Thank you very much for your time and help,I really appreciate it !
Log.e("newMessages2", "newMessages2: ${newMessages.value}")
return newMessages
}`
This code will return a null value because ondataChanged is not done yet. which means that you cannot return anything. Try observing newMessages instead
//in your viewModel
val messages = mainRepository.newMessages
fun getUsersChatList(){
mainRepository.getUserChatList()
}
//in your fragment
chatsListFragmentViewModel.getUserChatList()
chatsListFragmentViewModel.messages.observe(viewLifecycleOwner,object :
Observer<List<HashMap<String, Any?>>?> {
override fun onChanged(t: List<HashMap<String, Any?>>?) {
Log.e("userMessagesHashMap","userMessagesHashMap ${t}")
}
})
I'm just trying to find an answer how to pass the data from Repository to ViewModel without extra dependencies like RxJava. The LiveData seems as a not good solution here because I don't need to proceed it in my Presentation, only in ViewModel and it's not a good practice to use observeForever.
The code is simple: I use Firebase example trying to pass data with Flow but can't use it within a listener (Suspension functions can be called only within coroutine body error):
Repository
fun fetchFirebaseFlow(): Flow<List<MyData>?> = flow {
var ret: List<MyData>? = null
firebaseDb.child("data").addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val data = dataSnapshot.getValue<List<MyData>>()
emit(data) // Error. How to return the data here?
}
override fun onCancelled(databaseError: DatabaseError) {
emit(databaseError) // Error. How to return the data here?
}
})
// emit(ret) // Useless here
}
ViewModel
private suspend fun fetchFirebase() {
repo.fetchFirebaseFlow().collect { data ->
if (!data.isNullOrEmpty()) {
// Add data to something
} else {
// Something else
}
}
You can use callbackFlow
#ExperimentalCoroutinesApi
fun fetchFirebaseFlow(): Flow<List<String>?> = callbackFlow {
val listener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val data = dataSnapshot.getValue<List<MyData>>()
offer(data)
}
override fun onCancelled(databaseError: DatabaseError) {
}
}
val ref =firebaseDb.child("data")
reef.addListenerForSingleValueEvent(listener)
awaitClose{
//remove listener here
ref.removeEventListener(listener)
}
}
ObservableField is like LiveData but not lifecycle-aware and may be used instead of creating an Observable object.
{
val data = repo.getObservable()
val cb = object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(observable: Observable, i: Int) {
observable.removeOnPropertyChangedCallback(this)
val neededData = (observable as ObservableField<*>).get()
}
}
data.addOnPropertyChangedCallback(cb)
}
fun getObservable(): ObservableField<List<MyData>> {
val ret = ObservableField<List<MyData>>()
firebaseDb.child("events").addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
ret.set(dataSnapshot.getValue<List<MyData>>())
}
override fun onCancelled(databaseError: DatabaseError) {
ret.set(null)
}
})
return ret
}
It is also possible to use suspendCancellableCoroutine for a single result. Thanks to Kotlin forum.
I want to implement firebase realtime database with coroutines, so I need to use flow because firebase just accept callbacks. the problem is the .collect{} block never gets executed
here is my code
#ExperimentalCoroutinesApi
override suspend fun getProduct(barcode: String): ProductItem? {
return withContext(Dispatchers.Default) {
println("Hi")
var item: ProductItem? = null
productFlow(barcode).collect {
//this never gets called
print("Getting product")
item = it
}
println("Ending product request ${item?.name}")
Log.i("GetProduct",item?.name)
item
}
}
#ExperimentalCoroutinesApi
private fun productFlow(barcode: String): Flow<ProductItem?> = callbackFlow {
val database = FirebaseDatabase.getInstance()
val productRef = database.getReference("products/$barcode")
val callback = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
for(snapshot in dataSnapshot.children){
Log.i("Source", snapshot.value.toString())
}
val product = dataSnapshot.getValue(ProductItem::class.java)
Log.i("Source",product?.name) //everything is good until here
sendBlocking(dataSnapshot.getValue(ProductItem::class.java)) //after this i dont get anything on the collect{} block
}
override fun onCancelled(databaseError: DatabaseError) {
println("cancelling")
sendBlocking(null)
}
}
try {
productRef.addListenerForSingleValueEvent(callback)
} catch (e: FirebaseException) {
println("Firebase exception")
sendBlocking(null)
}
awaitClose{
println("Closing")
productRef.removeEventListener(callback)
}
}
First I would suggest to use the catch method to check if there is an error or not. Second, for callbackflow I remember using offer() instead of sendBlocking
the following snippet returns the result as 'null' on sequential code flow. I understand coroutines could be a viable solution to handle the callback asynchronously.
fun getUserProperty(path: String): String? {
var result: String? = null
database.child(KEY_USERS).child(getUid()).child(path)
.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onCancelled(error: DatabaseError) {
Log.e(TAG, "error: $error")
}
override fun onDataChange(snapshot: DataSnapshot) {
Log.w(TAG, "value: ${snapshot.value}")
result = snapshot.value.toString()
}
})
return result
}
Can the coroutines be of any help in this scenario to wait until the result of the callbacks (onDataChange()/onCancelled())?
Since the Firebase Realtime Database SDK doesn't provide any suspend functions, coroutines are not helpful when dealing with its APIs. You would need to convert the callback into a suspend function in order for you to be able to await the result in a coroutine.
Here's a suspend extension function that does this (I discovered a solution it by doing a google search):
suspend fun DatabaseReference.getValue(): DataSnapshot {
return async(CommonPool) {
suspendCoroutine<DataSnapshot> { continuation ->
addListenerForSingleValueEvent(FValueEventListener(
onDataChange = { continuation.resume(it) },
onError = { continuation.resumeWithException(it.toException()) }
))
}
}.await()
}
class FValueEventListener(val onDataChange: (DataSnapshot) -> Unit, val onError: (DatabaseError) -> Unit) : ValueEventListener {
override fun onDataChange(data: DataSnapshot) = onDataChange.invoke(data)
override fun onCancelled(error: DatabaseError) = onError.invoke(error)
}
With this, you now how a getValue() suspect method on DatabaseReference that can be awaited in a coroutine.
The #Doug example for singleValueEvent if you want to keep listing you can use coroutine flow like below:
#ExperimentalCoroutinesApi
inline fun <reified T> DatabaseReference.listen(): Flow<DataResult<T?>> =
callbackFlow {
val valueListener = object : ValueEventListener {
override fun onCancelled(databaseError: DatabaseError) {
close(databaseError.toException())
}
override fun onDataChange(dataSnapshot: DataSnapshot) {
try {
val value = dataSnapshot.getValue(T::class.java)
offer(DataResult.Success(value))
} catch (exp: Exception) {
Timber.e(exp)
if (!isClosedForSend) offer(DataResult.Error(exp))
}
}
}
addValueEventListener(valueListener)
awaitClose { removeEventListener(valueListener) }
}
In case anyone still uses the original answer's code but needs to update it to match the non-experimental version of Coroutines here's how I changed it:
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.ValueEventListener
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
suspend fun DatabaseReference.getSnapshotValue(): DataSnapshot {
return withContext(Dispatchers.IO) {
suspendCoroutine<DataSnapshot> { continuation ->
addListenerForSingleValueEvent(FValueEventListener(
onDataChange = { continuation.resume(it) },
onError = { continuation.resumeWithException(it.toException()) }
))
}
}
}
class FValueEventListener(val onDataChange: (DataSnapshot) -> Unit, val onError: (DatabaseError) -> Unit) : ValueEventListener {
override fun onDataChange(data: DataSnapshot) = onDataChange.invoke(data)
override fun onCancelled(error: DatabaseError) = onError.invoke(error)
}
Then using it would be as simple as: val snapshot = ref.getSnapshotValue()
Update
I also needed to observe a node and used Omar's answer to do it. If anyone needs an example of how to use it here it is:
#ExperimentalCoroutinesApi
inline fun <reified T> DatabaseReference.listen(): Flow<T?>? =
callbackFlow {
val valueListener = object : ValueEventListener {
override fun onCancelled(databaseError: DatabaseError) {
close()
}
override fun onDataChange(dataSnapshot: DataSnapshot) {
try {
val value = dataSnapshot.getValue(T::class.java)
offer(value)
} catch (exp: Exception) {
if (!isClosedForSend) offer(null)
}
}
}
addValueEventListener(valueListener)
awaitClose { removeEventListener(valueListener) }
}
Then to call it inside an Activity or Fragment you would create your listener like so:
var listener = FirebaseUtils.databaseReference
.child(AppConstants.FIREBASE_PATH_EMPLOYEES)
.child(AuthUtils.retrieveUID()!!).listen<User>()
Then call it inside your function:
CoroutineScope(IO).launch {
withContext(IO) {
listener?.collect{
print(it)
}
}
}
And then dispose inside onStop():
override fun onStop(){
listener = null
super.onStop()
}