callbackFlow not returning anything android kotlin - android

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

Related

How to replace callbacks in coroutines android

here is my code, I am using it for logging in user with google,
This is my viewModel code
fun signInWithGoogle(account: GoogleSignInAccount): LiveData<Resource<Any>> {
val credential = GoogleAuthProvider.getCredential(account.idToken, null)
return liveData (IO){
authRepo.firebaseSignInWithGoogle(credential, object : FetchUser {
override suspend fun onUserDataFetch(user: User) {
this#liveData.emit(Resource.success(user))
}
override suspend fun onError(error: AppError?) {
this#liveData.emit(Resource.error(error, null))
}
})
}
}
This is my code authRepository where i am logging the user in and checking if user already exits in database or not according to that performing the work
suspend fun firebaseSignInWithGoogle(googleAuthCredential: AuthCredential, userCallBack: FetchUser) {
coroutineScope {
firebaseAuth.signInWithCredential(googleAuthCredential).await()
createUpdateUser(userCallBack)
}
}
private suspend fun createUpdateUser(userCallBack: FetchUser) {
val firebaseUser = firebaseAuth.currentUser
if (firebaseUser != null) {
userIntegrator.getUserById(firebaseUser.uid, object : OnDataChanged {
override suspend fun onChanged(any: Any?) {
if (any != null && any is User) {
any.isNew = false
userIntegrator.createUpdateUser(any, userCallBack)
} else {
val user = User()
user.id = firebaseUser.uid
user.name = firebaseUser.displayName
user.email = firebaseUser.email
user.isNew = true
userIntegrator.createUpdateUser(
user,
userCallBack
)
}
}
})
}
}
This is my last class where I am updating the user in database
suspend fun createUpdateUser(user: User, userCallBack: FetchUser) {
if (user.id.isNullOrEmpty()) {
userCallBack.onError(AppError(StatusCode.UnSuccess, ""))
return
}
val dp = databaseHelper.dataFirestoreReference?.collection(DatabaseHelper.USERS)?.document()
dp?.apply {
dp.set(user.toMap()).await().apply {
dp.get().await().toObject(User::class.java)?.let {
userCallBack.onUserDataFetch(it)
}?: kotlin.run {
userCallBack.onError(AppError(StatusCode.Exception,"Unable to add user at the moment"))
}
}
}
}
Now here whole thing is that, I am using a FetchUser interface which look like this
interface FetchUser {
suspend fun onUserDataFetch(user: User)
suspend fun onError(error: AppError?)
}
I just want to get rid of it and looking for something else in coroutines.
Also I just wanted to know the best practice here,
What should I do with it.
Also I want to make it unit testable
There are 2 ways, if you want to call and get result directly, you could use suspendCoroutine. Otherway, if you want to get stream of data like, loading, result, error,... you could try callbackFlow
Exp:
suspend fun yourMethod() = suspendCoroutine { cont ->
// do something
cont.resume(result)
}
suspend fun yourMethod() = callbackFlow {
val callbackImpl = object: yourInterace {
// your implementation
fun onSuccess() {
emit(your result)
}
fun onFailed() {
emit(error)
}
}
handleYourcallback(callbackImpl)
}

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.

Coroutin To Perform task

hi this is my user repository
class UserRepository(private val appAuth: FirebaseAuth) : SafeAuthRequest(){
suspend fun userLogin(email: String,password: String) : AuthResult{
return authRequest { appAuth.signInWithEmailAndPassword(email,password)}
}
}
this is the SafeAuthRequest class
open class SafeAuthRequest {
suspend fun<T: Any> authRequest(call : suspend () -> Task<T>) : T{
val task = call.invoke()
if(task.isSuccessful){
return task.result!!
}
else{
val error = task.exception?.message
throw AuthExceptions("$error\nInvalid email or password")
}
}
}
calling above things like that
/** Method to perform login operation with custom */
fun onClickCustomLogin(view: View){
authListener?.onStarted()
Coroutines.main {
try {
val authResult = repository.userLogin(email!!,password!!)
authListener?.onSuccess()
}catch (e : AuthExceptions){
authListener?.onFailure(e.message!!)
}
}
}
and my authListener like this
interface AuthListener {
fun onStarted()
fun onSuccess()
fun onFailure(message: String)
}
I am getting an error as the task is not completed
is the correct way to implement the task
I'm using MVVM architectural pattern, so the example I'm going to provide is called from my ViewModel class, that means I have access to viewModelScope. If you want to run a similar code on Activity class, you have to use the Coroutines scope available for your Activity, for example:
val uiScope = CoroutineScope(Dispatchers.Main)
uiScope.launch {...}
Answering your question, what I've done to retrieve login from user repository is this:
//UserRepository.kt
class UserRepository(private val appAuth: FirebaseAuth) {
suspend fun userLogin(email: String, password: String) : LoginResult{
val firebaseUser = appAuth.signInWithEmailAndPassword(email, password).await() // Do not forget .await()
return LoginResult(firebaseUser)
}
}
LoginResult is a wrapper class of firebase auth response.
//ClassViewModel.kt
class LoginFirebaseViewModel(): ViewModel(){
private val _loginResult = MutableLiveData<LoginResult>()
val loginResult: LiveData<LoginResult> = _loginResult
fun login() {
viewModelScope.launch {
try {
repository.userLogin(email!!,password!!).let {
_loginResult.value = it
}
} catch (e: FirebaseAuthException) {
// Do something on firebase exception
}
}
}
}
The code on Activity class would be like this:
// Function inside Activity
fun onClickCustomLogin(view: View){
val uiScope = CoroutineScope(Dispatchers.Main)
uiScope.launch {
try {
repository.userLogin(email!!,password!!).let {
authResult = it
}
}catch (e : FirebaseAuthException){
// Do something on firebase exception
}
}
}
One of the main benefits of using Coroutines is that you convert asynchronous code in sequential one. That means you don't need listeners or callbacks.
I hope this help you

how to handle callback using kotlin coroutines

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

Categories

Resources