how to handle callback using kotlin coroutines - android

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

Related

RealtimeDatabase: Where should I unsubscribe from EventListener?

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

Return data from Repository to ViewModel without LiveData

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.

How do i fix "Job was Cancelled" exception?

In my Fragment, I'm trying to fetch data from Firebase Database using coroutines where data is retrieving properly. Here is my code
#ExperimentalCoroutinesApi //Fragment Class code
override fun onStart() {
super.onStart()
checkOutViewModel.viewModelScope.launch {
try{
if (isActive){
checkOutViewModel.getCartDataFromFirebaseNetwork().collect{
tempList.add(it)
}
}
}catch (ex : Exception){
Log.d("exception message",ex.cause?.message!!) //Fatal Exception: Main
}
orderListAdapter?.submitList(tempList)
binding.progress.visibility = View.GONE
binding.recycler.visibility = View.VISIBLE
}
}
#ExperimentalCoroutinesApi //Viewmodel class code
suspend fun getCartDataFromFirebaseNetwork()= firebaseNetwork.getCartFromFirebase()
#ExperimentalCoroutinesApi //Repository class code
suspend fun getCartFromFirebase() = callbackFlow<Cart> {
ensureActive()
val counterList = myFlow.toList()
val itemList = myFlow.mapBasketToItemsList().toList()
val pairs = myFlow.mapBasketListToQuantity().toList()
if(itemList.isNotEmpty() && pairs.isNotEmpty()){
for ((current,item) in itemList.withIndex()) {
val cart = Cart(counterList[current].basketId!!,item.id!!,item.url!!,item.name!!,pairs[current].first,pairs[current].second,counterList[current].itemCounter!!,pairs[current].second)
offer(cart)
}
channel.close()
}
}
#ExperimentalCoroutinesApi
val myFlow = callbackFlow<Basket> {
databaseReference.child("Cart").child(getCurrentUserUid())
.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onCancelled(p0: DatabaseError) {
}
override fun onDataChange(dataSnapshot: DataSnapshot) {
if (dataSnapshot.exists()) {
for (data in dataSnapshot.children) {
val basket = Basket()
basket.basketId = data.key
basket.itemId = data.child("itemId").value as String
basket.itemCounter = data.child("itemCounter").value as String
basket.itemWeight = data.child("itemWeight").value as String
offer(basket)
}
channel.close()
}
}
})
awaitClose()
}
#ExperimentalCoroutinesApi
private fun Flow<Basket>.mapBasketToItemsList() : Flow<Items> = map{basket ->
suspendCoroutine<Items> {continuation ->
databaseReference.child("Items").child(basket.itemId!!)
.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onCancelled(p0: DatabaseError) {
}
override fun onDataChange(dataSnapshot: DataSnapshot) {
if (dataSnapshot.exists()) {
val items = dataSnapshot.getValue(Items::class.java)!!
continuation.resume(items)
}
}
})
}
}
#ExperimentalCoroutinesApi
private fun Flow<Basket>.mapBasketListToQuantity() : Flow<Pair<String,String>> = map{basket ->
suspendCoroutine<Pair<String,String>> {continuation ->
databaseReference.child("Quantities").child(basket.itemId!!)
.child(basket.itemWeight!!)
.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onCancelled(p0: DatabaseError) {
}
override fun onDataChange(dataSnapshot: DataSnapshot) {
if (dataSnapshot.exists()) {
val key = dataSnapshot.key
val value = dataSnapshot.value as String
val myPair = Pair(key!!, value)
continuation.resume(myPair)
}
}
})
}
}
Edited:
This is my Navigation Flow of Fragments
OnBoarding-Authentication-MainFragment-CheckItemListFragment
override fun onStart() { //OnBoarding Fragment
super.onStart()
try {
if(viewModel.checkAuth()){
updateUI()
}
}catch (ex : Exception){
println("In onBoarding Fragment")
Log.d("exception message",ex.cause?.message!!)
}
}
override fun onStart() { //Authentication Fragment
super.onStart()
try {
if(mAuth.currentUser == null){
showShortToast("Please Login")
}else{
updateUI()
}
}catch (ex : Exception){
println("In authentication Fragment")
Log.d("exception message",ex.cause?.message!!)
}
}
override fun onStart() { //MainFragment
super.onStart()
try {
if(mainFragmentViewModel.checkSignIn() == null)
findNavController().navigateUp()
binding.toolbar.add_to_cart.setOnClickListener {
it.findNavController().navigate(R.id.action_mainFragment_to_checkoutItemsList)
}
}catch (ex : Exception){
println("In Main Fragment")
Log.d("exception",ex.message!!)
}
}
#ExperimentalCoroutinesApi
override fun onStart() { //CheckItemList Fragment
super.onStart()
try {
binding.addToCart.setOnClickListener {
checkOutViewModel.viewModelScope.launch {
val message = orderListAdapter?.getList()?.let { it1 -> checkOutViewModel.submitFinalCart(it1) }
if(message!!){
findNavController().navigate(R.id.action_checkoutItemsList_to_finalCarts)
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
ensureActive()
checkOutViewModel.getCartDataFromFirebaseNetwork().collect {
tempList.add(it)
orderListAdapter?.submitList(tempList)
binding.progress.visibility = View.GONE
binding.recycler.visibility = View.VISIBLE
}
}
}catch (ex : Exception){
println("In checkItemList Fragment")
Log.d("exception message",ex.cause?.message!!)
}
}
Edited : My Logcat is :-
--------- beginning of crash
07-10 21:18:40.605 30715-30715/com.example.groceryapp E/AndroidRuntime: FATAL
EXCEPTION: main
Process: com.example.groceryapp, PID: 30715
f.d
at com.example.groceryapp.checkout.CheckoutItemsList$e.a(:73)
at f.z.k.a.a.b(:33)
at kotlinx.coroutines.u0.run(:334)
at kotlinx.coroutines.z0.k(:68)
at kotlinx.coroutines.r0.b(:354)
at f.z.i.b(:42)
at com.example.groceryapp.f.a$r$a$b.a(:262)
at com.google.firebase.database.m$a.a(:179)
at com.google.firebase.database.u.a0.a(:75)
at com.google.firebase.database.u.i0.d.a(:63)
at com.google.firebase.database.u.i0.g$a.run(:55)
at android.os.Handler.handleCallback(Handler.java:742)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:157)
at android.app.ActivityThread.main(ActivityThread.java:5603)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:774)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:652)
This is how i used my Try Catch block in every Fragments to handle this issue but it is also not working at all. I'm also using isActive method to check whether the job is still active or not before retrieving any data. I get "Fatal Exception: Main, Job was Cancelled" if i pressed back button before recyclerview shows the data. This exception only comes if i use callback flow. Is there any way to handle this issue or is it a bug in callback flow?. So far I couldn't find any possible answer that will solve my issue. Please tell me how do i fix it?
I faced with the same issue before and here is my solution with an extension of SendChannel
fun <T> SendChannel<T>.offerCatching(element: T): Boolean {
return runCatching { offer(element) }.getOrDefault(false)
}
and when emit event just call offerCatching
Why you launch coroutine in fragment onStart with viewModelScope ?
in fragment/activity you should use lifecycleScope.
see here for more details.
Wrap offer() method into try..catch and capture CancellationException. You may create an extension function to use across the application where using offer() method.

callbackFlow not returning anything android kotlin

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

Call onComplete() in setCancellable

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

Categories

Resources