Skip to content

Instantly share code, notes, and snippets.

@sergiocasero
Last active December 13, 2023 10:16
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sergiocasero/eca986497c2025d703074cc995ed3618 to your computer and use it in GitHub Desktop.
Save sergiocasero/eca986497c2025d703074cc995ed3618 to your computer and use it in GitHub Desktop.
Firebase wrapper for iOS and Android with kotlin multiplatform

Hello guys, I've raised into that problem for a days, but i think I've found one solution, maybe this isn't the better way to do that, but... I will share it. (This is inspired in the Firebase multiplatform repo -> https://github.com/RubyLichtenstein/Kotlin-Multiplatform-Firebase, I'm just summarizing that)

If you want to call some code from a DataSource or Repository from your common code, and the libraries you're using use callbacks, you need to wrap them and there are some problems:

Let me explain it with an example:

expect class RemoteDataSource() {
   suspend fun getUser(): Either<Error, User>
}

In Android, this is pretty easy to solve, we can do something like this:

actual class RemoteDataSource() {
   suspend fun getUser(): Either<Error, User> = 
      try { Either.Right(User(email = FirebaseAuth.getInstance().signIn().await().email) }
      catch (e: Exception) { Either.Left(Error.Default) }
}

But.. the problem comes from iOS, we can't implement suspend functions on Swift and iOS (Or I don't know how to do that), so... we have a problem

The solution I've found is this one (Still WiP):

  1. Declare a result callback
interface Result<T> {
    fun onComplete(success: Either.Right<Error, T>)
    fun onError(error: Either.Left<Error, T>)
}

(Don't focus on Either, this is because I'm using this FP container)

  1. Create this function
suspend fun <T> await(result: (Result<T>) -> Unit): Either<Error, T> =
    suspendCancellableCoroutine { cont ->
        result(object : Result<T> {
            override fun onComplete(success: Either.Right<Error, T>) {
                print("onComplete")
                cont.resume(success)
            }

            override fun onError(error: Either.Left<Error, T>) {
                print("onError")
                cont.resume(error)
            }
        })
    }
  1. Create a Bridge (worst name ever):
interface RemoteBridge {
    fun getUser(result: Result<User>)
}

And... the datasource will looks like this:

actual class RemoteDataSource(private val bridge: RemoteBridge) {
    actual suspend fun getUser(): Either<Error, User> = await { bridge.getUser(it) }
}

Finally, this is the result from the iOS side:

class IosBridge: RemoteBridge {
    
    private lazy var auth: Auth = Auth.auth()
    
    func createUser(result: Result) {
        auth.signInAnonymously {result, error in
            if(error == nil) { // Just an example, to show the two calls
                result.onComplete(success: Either.Right(success: Success()))
            } else {
                result.onError(error: Either.Left(error: Error.Default.self))
            }
        }
    }

This is the best solution I've found from now, if you have another one, please share it!!

This solution comes to another problem, and is taht you will need to specify the dispatcher for every, imagine that you have some thing like this:

class CommonRepo(private val network, private val realtime): Repo {
    fun getUser(): Either<Error, User> = realtime.getUser().flatMap { withContext(executor.bg) { network.saveUser(it) } }

}```

To call the "network" call you need to do it in background, and you manually need to specify that because the "callback" system is not ready to work in another thread.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment