Table of contents
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()
}
}
- 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{}
orcallbackflow{}
. 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")
}
}
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")))
}
}
- 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 {
}
-
Notice how we have just hardcoded
FirebaseAuth
. This not only makes it harder to test but it also makes the code strictly dependant onFirebaseAuth
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).- To get around this we have to create a wrapper class for FirebaseAuth. Which is really just us trying to program to the interface not the implementation.
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>>
}
- 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()
}
Notice how this wrapper class still internally calls to
FirebaseAuth
, which is ok because we will doing so through theauthRegister
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
}
}
- 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 (0)