CodeNewbie Community 🌱

Cover image for The proper way to integrate Firebase into your Android app
Tristan
Tristan

Posted on

The proper way to integrate Firebase into your Android app

Table of contents

  1. What we are talking about
  2. Spaghetti code example
  3. Exception handling
  4. Wrapper class

My app on the Google Playstore

GitHub code

Introduction

  • This series will be an informal demonstration of any problems I face or any observations I make while developing Android apps. Each blog post in this series will be unique and separate from the others, so feel free to look around.

What we are going to talk about

  • So if you are like me and starting to fortify your application with tests. One of the first layers you will want to test is the repository/data layer. If you have FireBase Authentication integrated into your code, you might run into a bit of a problem.

The spaghetti way (not good)

  • This way of implementing FireBase is what you will typically see in spaghetti code (version one of my code base does this):
class AuthRepositoryImpl(
    private val auth: FirebaseAuth = Firebase.auth

): AuthRepository {

    override fun authRegister(email: String, password: String): Flow<Response<Boolean>> = callbackFlow {

     try{
            trySend(Response.Loading)
            auth.createUserWithEmailAndPassword(email,password)
                .addOnCompleteListener { task ->

                    if(task.isSuccessful){

                        trySend(Response.Success(true))

                    }else{

                        trySend(Response.Failure(Exception()))
                    }

                }



}catch(e:Exception){
     trySend(Response.Failure(Exception()))
}



        awaitClose()
    }
}

Enter fullscreen mode Exit fullscreen mode
  • yummy spaghetti code but there are two main problems:

1) The exception handling
2) No wrapper class for FirebaseAuth

Exception Handling

  • When dealing with flows a big no no is having a try/catch block inside a flow builder, like flow{} or callbackflow{}. As this creates the possibility of it producing a side effect of catching downstream exceptions(exceptions happening when collect{} is called)

  • By having the try/catch block inside of a flow builder, we are also violating the Separation of concerns principle. As the code now has a side effect on the code that collects the flow

  • The try/catch block should ONLY!!! be used to surround the collector to handle exceptions raised from the collector its self. If you are using the try catch block it should look like this:



fun main() ={
    try {
        upstreamFlow.collect { value ->

            if(value <= 4) {
                "Collected $value while we expect values below 2"
            }else{
              throw RuntimeException()

            }
        }
    } catch (e: Throwable) {
        println("Caught $e")
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Notice how we are not handling exceptions that are produced inside the flow. Only exceptions that we created during collection.

  • But then how do we catch exceptions inside a flow? The answer to that question is the catch operator

Catch operator

  • The catch operator allows us to ONLY!! catch upstream exceptions(exceptions from the flow). Which means we can transform our first block of code to this:
   override suspend fun authRegister(email: String, password: String) = callbackFlow {


            trySend(Response.Loading)

            auth.createUserWithEmailAndPassword(email,password)
                .addOnCompleteListener { task ->

                    if(task.isSuccessful){

                        trySend(Response.Success(true))

                    }else{

                        trySend(Response.Failure(Exception()))
                    }

                }

        awaitClose()
    }.catch { cause: Throwable->
        if(cause is FirebaseAuthWeakPasswordException){
            emit(Response.Failure(Exception("Stronger password required")))
        }
        if(cause is FirebaseAuthInvalidCredentialsException){
            emit(Response.Failure(Exception("Invalid credentials")))
        }
        if(cause is FirebaseAuthUserCollisionException){
            emit(Response.Failure(Exception("Email already exists")))
        }
        else{
            emit(Response.Failure(Exception("Error! Please try again")))
        }
    }

Enter fullscreen mode Exit fullscreen mode
  • From the code block above you will also notice that we are Materializing our exceptions. Which means we are transforming them into something our code can handle.

2) No wrapper class for FirebaseAuth

  • Typically when we first add FireBase to our repository layer, it will look something like this:
class AuthRepositoryImpl(
    private val auth: FirebaseAuth = Firebase.auth
): AuthRepository {

}

Enter fullscreen mode Exit fullscreen mode
  • Notice how we have just hardcoded FirebaseAuth. This not only makes it harder to test but it also makes the code strictly dependant on FirebaseAuth and all of its methods. Which means if we want to switch from one authentication service to another, we would have to rewrite all of our code to deal with the new authentication service(which is not fun).

Create the interface

  • The first thing we have to do is to create an interface:
interface AuthenticationSource {

fun authRegister(email: String, password: String, username: String): Flow<Response<Boolean>>

}

Enter fullscreen mode Exit fullscreen mode
  • Then we can use this interface to create the wrapper class

Create the wrapper class:

class FireBaseAuthentication : AuthenticationSource {

    private val auth: FirebaseAuth = Firebase.auth

override fun authRegister(email: String, password: String): Flow<Response<Boolean>> = callbackFlow {


        trySend(Response.Loading)

        auth.createUserWithEmailAndPassword(email,password)
            .addOnCompleteListener { task ->

                if(task.isSuccessful){

                    trySend(Response.Success(true))

                }else{

                    trySend(Response.Failure(Exception()))
                }

            }

        awaitClose()
    }

Enter fullscreen mode Exit fullscreen mode
  • Notice how this wrapper class still internally calls to FirebaseAuth, which is ok because we will doing so through the authRegister method.

  • You may also noticed that we are not handling exceptions here, as I have decided to do that inside of the repository layer. Not sure if this is the right call, but I am sticking to it now. I also believe that the repository layer is where you should implement any Intermediate flow operators

Replace the hard coded implementation with the wrapper class

class AuthRepositoryImpl(

    private val auth:AuthenticationSource  = FireBaseAuthentication()

): AuthRepository {
   override fun registerUser(email:String,password: String): Flow<Response<Boolean>> {

        val items = auth.authRegister(email, password)
            .catch { cause: Throwable->
                if(cause is FirebaseAuthWeakPasswordException){
                    emit(Response.Failure(Exception("Stronger password required")))
                }
                if(cause is FirebaseAuthInvalidCredentialsException){
                    emit(Response.Failure(Exception("Invalid credentials")))
                }
                if(cause is FirebaseAuthUserCollisionException){
                    emit(Response.Failure(Exception("Email already exists")))
                }
                else{
                    emit(Response.Failure(Exception("Error! Please try again")))
                }
            }
        return items
    }


}

Enter fullscreen mode Exit fullscreen mode
  • Since our code above now goes through the AuthenticationSource interface, it is now flexible and we can easily swap it out with a new authentication source that implements this interface. Also this code has not become 100% more test friendly.

Conclusion

  • Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.

Top comments (2)

Collapse
 
methson profile image
cogim

Add the Firebase dependencies to your project's build.gradle file.
Connect your app to Firebase by adding the Firebase initialization code in your app's main activity.
Use the Firebase APIs in your app's code to interact with features like the real-time database or authentication.
(Note: The given phrase "indian train simulator 2018 mod apk" seems unrelated to the topic of Firebase integration. If you have any specific questions about it, please let me know.)

Collapse
 
theplebdev profile image
Tristan

Please, no one click the link!!!! I have reported this comment and @cogim for inappropriate and malicious advertisement