Search code examples
androidkotlingoogle-cloud-firestorekotlin-android-extensionssealed-class

Firestore (Kotlin) proper way to convert a complete task into a sealed class when performing a document request


I'm trying to experiment a proper way to convert a complete task into a sealed class easy to read when performing a get request on a document (at this time and I will see later for collections request).

import com.google.android.gms.tasks.Task
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.FirebaseFirestoreException
import timber.log.Timber

fun <T> Task<DocumentSnapshot?>.toDocumentResult(parser: (documentSnapshotExisting: DocumentSnapshot) -> T): DocumentResult<T>?{
    val documentResult: DocumentResult<T> = if(isSuccessful){
        val documentSnapshot: DocumentSnapshot = result!!
        if(documentSnapshot.exists()){
            try {
                DocumentResult.Found(parser.invoke(documentSnapshot))
            }
            catch (e: java.lang.Exception){
                DocumentResult.ParserException<T>(documentId = documentSnapshot.id, e = e)
            }
        }else{
            DocumentResult.NotFound(documentSnapshot.id)
        }
    }else{
        DocumentResult.Error(exception!! as FirebaseFirestoreException)
    }
    documentResult.log()
    return documentResult
}


sealed class DocumentResult<T>{
    abstract fun log()

    class Found<T>(val o: T): DocumentResult<T>() {
        override fun log() {
            Timber.tag("DocumentResult").w("$o")
        }
    }

    class NotFound<T>(val documentId: String): DocumentResult<T>() {
        override fun log() {
            Timber.tag("DocumentResult").w("documentId: $documentId doesn't exist")
        }
    }

    class ParserException<T>(val documentId: String, val e: Exception): DocumentResult<T>() {
        override fun log() {
            Timber.tag("DocumentResult").e("ParserException: ${e.localizedMessage?:e.message?:"error"}, documentId: $documentId")
        }
    }

    class Error<T>(val e: FirebaseFirestoreException): DocumentResult<T>() {
        override fun log() {
            Timber.tag("DocumentResult").e("FirebaseFirestoreException - code: ${e.code.name}, ${e.localizedMessage?:e.message?:"error"}")
        }
    }
}

With this snippet, I can do this :

activity.firestore.documentAvailableLanguages().get().addOnCompleteListener { task ->
    val documentResult = task.toDocumentResult { AvailableLanguages.toObject(it) }
    when(documentResult){
        is DocumentResult.Found -> { /* My converted object */ }
        is DocumentResult.NotFound -> {  /* document not found */}
        is DocumentResult.Error-> {  /* FirebaseFirestoreException */}
        is DocumentResult.ParserException -> { /* Conversion didn't work, exception */ }
    }
}

My question is :

1) Can we reasonably ensure that Task.exception is always not null and instance of FirebaseFirestoreException when isSuccessFul is false ?

2) Are we sure that task.result is always not null when task.isSuccessful is true ?

Thanks in advance


Solution

  • For both questions, please note that a Task is "successful" when the work represented by the task is finished as expected, with no errors. On the orter side, a Task is "complete" when the work represented by the Task is finished, regardless of its "success" or "failure". There may be or may or may be not an error, you'll have to check for that.

    An already successfully completed Task returns a DocumentSnapshot which will never have the value of null. If the requested document does not exist, you'll get an empty DocumentSnapshot object not null. This also means that if you'll call exists():

    documentSnapshot.exists() //Will returns false
    

    And if are calling getData() method:

    documentSnapshot.getData() //An exception will be thrown
    

    If the Taks is not "successful", the Exception that is trown by task.getException() is an instanceof FirebaseFirestoreException. Please note that Task's getException() method:

    Returns the exception that caused the Task to fail. Returns null if the Task is not yet complete, or completed successfully.