0

I want to check if my database write was successful in order to show the user an error message.

My current approach doesn't work as it says "Type mismatch, required Unit found EmailStatus"

Current approach

class EmailRepositoryImpl : EmailRepository {
    private val db = Firebase.firestore

    override fun sendEmail(email: Email): EmailStatus<Nothing> {
        db.collection("emails").document().set(email).addOnCompleteListener {
            if (it.isSuccessful) return@addOnCompleteListener EmailStatus.Success<Nothing>
            if (it.isCanceled) return@addOnCompleteListener EmailStatus.Error(it.exception!!)
        }
    }
}

Status Sealed Class

sealed class EmailStatus<out T> {
    data class Success<out T>(val data: T) : EmailStatus<T>()
    data class Error(val exception: Exception) : EmailStatus<Nothing>()
}

Is it even possible to write something like this? As far as I know there is a generic firebase error type but I didn't found anything related to kotlin or android...

I appreciate every help, thank you

Edit

I've tried getting my document, but I am just getting null: (When I use the listener approach, everything works fine)

Interface

interface EmailRepository {
    suspend fun getEmail(): Flow<EmailEntity?>
}

Interface Implementation

override suspend fun getEmail(): Flow<EmailEntity?> = flow {
    val result = db.collection("emailprice").document("Email").get().await()
    emit(result.toObject<EmailEntity>())
}

ViewModel

private val emailEntity = liveData<EmailEntity?>(Dispatchers.IO) {
    emailRepository.getCalibratePrice()
}
Andrew
  • 4,264
  • 1
  • 21
  • 65

2 Answers2

2

The problem is that addOnCompleteListener callback does not return anything (Unit) and you are trying to return an EmailStatus from that scope.

You have three approaches:

  1. Create an interface that will populate the value and return that EmailStatus down to your caller layer
  2. Use Coroutines to suspend this function when the async call to firebase is done and then return that value
  3. Use Flow to offer the data when it's ready to process

I think the easiest way to do this one shot operation is to use Coroutines; I have written an article about that.

halfer
  • 19,824
  • 17
  • 99
  • 186
Gastón Saillén
  • 12,319
  • 5
  • 67
  • 77
  • Thank you really much for your quick response. But I have a problem: Since I am not getting any data from Firebase but writing to it, how do I know that this write is succeeded and therefore emit Resource.Sucess? – Andrew Sep 16 '20 at 15:00
  • @Andrew You can't return a success value immediately from the function because the Firebase API is asynchronous and returns immediately before the operation is complete. You will have to pick one of the options in the answer here. – Doug Stevenson Sep 16 '20 at 15:29
  • Okay, thank you. I've implemented the methods from the article and I get my documents now as "LiveData>". How am I able to get the EmailEntity Data INSIDE of my Viewmodel to use it? – Andrew Sep 16 '20 at 15:43
  • the problem with LiveData is that you are attaching now livedata to your repository which is not really suggested, what you can do is to implement coroutines with the post I shared above, there you can use the google-play-services-coroutine library to do .await() on any async work in Firebase – Gastón Saillén Sep 16 '20 at 16:18
  • @GastonSailen Okay thank you. I have done that, but I am only getting null. I will investigate in this. – Andrew Sep 16 '20 at 17:21
  • Instead of returning EmailStatus(Nothing) return EmailStatus(Unit) – Gastón Saillén Sep 16 '20 at 17:48
  • yes, you need to modify the return type of the EmailStatus class to return Unit – Gastón Saillén Sep 16 '20 at 18:42
  • Getting data from server works, but what if you want to verify that a write is successful using coroutines instead of callbacks? – Red M May 02 '23 at 22:51
0

Okay, this is the final solution, thanks to @Gastón Saillén and @Doug Stevenson :

EmailRepository

interface EmailRepository {
    fun sendEmail(email: Email): Flow<EmailStatus<Unit>>
}

EmailRepository Implementation

class EmailRepositoryImpl @Inject constructor(
    private val db: FirebaseFirestore
) : EmailRepository {

    override fun sendEmail(email: Email)= flow<EmailStatus<Unit>> {
        db.collection("emails").add(email).await()
        emit(EmailStatus.success(Unit))
    }.catch {
        emit(EmailStatus.failed(it.message.toString()))
    }.flowOn(Dispatchers.Main)

}

ViewModel

fun sendEmail(): LiveData<EmailStatus<Unit>> {
    val newEmail = createEmail()
    return emailRepository.sendEmail(newEmail).asLiveData()
}

Fragment

btn.setOnClickListener {
            viewModel.sendEmail().observe(viewLifecycleOwner) {
                when(it) {
                    is EmailStatus.Success -> {
                        valid = true
                        navigateTo(next, bundleNext)
                        Toast.makeText(requireContext(), "Success", Toast.LENGTH_SHORT).show()
                    }
                    is EmailStatus.Failure -> {
                        valid = false
                        Toast.makeText(requireContext(), "Failed ${it.message}", Toast.LENGTH_SHORT).show()
                    }
                }
            }
}

The only problem I currently have is that my "faield state" does not work like it should.

It should fail, if the user has no internet access. Currently, the write to the db never fails and Firebase just waits till the user has internet access. The problem here is that when I click multiple times, the write is executed multiple times. But I think I have to implement a bit more logic here and the above written code is fine like it currently is.

Andrew
  • 4,264
  • 1
  • 21
  • 65