Last Week

Supabase auth is implemented with One-Time Passcode (OTP).

Example of functional supabase auth

Tools used:

What does it mean in English?

  • An Authentication Service makes sure the user is who they say they are. In this particular implementation, when a user tries to login with their email address, an authentication server (i.e., a powerful computer) sends a random number to the user’s email address. Since the user is the only one who can access this email address (ideally), they are the only one who can provide it to the authentication server. Ergo, they (should be) are who they say they are.

  • A Dependency Injection Library simplifies the task of following the dependency injection design pattern. Instead of writing the special code to enable dependency injection, the library provides a convenient set of tools to use to accomplish the goal.

  • The Dependency Injection Design Pattern structures the code in such a way that tools and components needed for something to work are injected systemically as opposed to being created manually. Imagine you are changing a tire on a car. You would need some tools (e.g., socket wrench). In a manual approach (i.e., not using dependency injection), every time you need to use (read: depend on) the socket wrench you would need to find it. This takes more time and you may not even find the right size (or it’s broken or otherwise unsuitable), causing you to search for it again. The dependency injection library is like a handy butler standing right next to you while you change the tire. As soon as you need to use the socket wrench, the butler knows the correct tool you need (since they can see the nut you are trying to loosen) and hands it (read: injects) to you immediately. This can save a lot of time on larger projects. Additionally, because the butler knows the exact tool you will need for any given moment, you no longer have to worry about acquiring them. The butler does not have to give you the socket wrench you have in the garage. It can be the neighbor’s. It can be borrowed from Home Depot. It makes you less dependent (i.e., loose coupling) on the tool/components you have been using. Further, you also don’t need a socket wrench for every nut, the butler knows a single wrench can loosen and tighten many nuts before breaking so the butler keeps just one copy of it in the garage, saving you space.

Nerdy Details

How did I use Koin Dependency Injection to set up Supabase Auth?

Before we begin, we need to include the necessary dependencies. See guide here.

Once the dependencies are added and Gradle sync is complete, we need to make an interface for the repository. Note that they are suspend functions since Supabase functions are themselves (potentially long-running) suspend functions.

interface UserRepository {
    suspend fun signIn(email: String): Result<Unit>
}

We then implement the repository using Supabase. We pass in Dispatchers.IO since we are implementing suspend functions for IO operations.

class SupabaseUserRepositoryImpl(
    private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : UserRepository {
    override suspend fun signIn(email: String): Result<Unit> {
        TODO("Not yet implemented")
    }
}

To simplify function syntax at call sites within the ViewModels, we use a handy helper function for error handling.

suspend fun <T> makeApiCall(
    dispatcher: CoroutineDispatcher,
    call: suspend () -> T
): Result<T> = runCatching {
    withContext(dispatcher) {
        call.invoke()
    }
}

Before we implement the functions, we need an instance of Supabase client.

class SupabaseUserRepositoryImpl(
    private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : UserRepository {
	// ...
	private val supabaseClient = createSupabaseClient(
	    supabaseUrl = "https://your_supabase_instance.supabase.co",
	    supabaseKey = "your_supabase_key"
	) {
	    install(Auth)
	}
	// ...
}

Now we implement the functions. Here you can see that we use the helper makeApiCall() function defined earlier.

class SupabaseUserRepositoryImpl(
    private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : UserRepository {
	// ...
	override suspend fun signIn(email: String): Result<Unit> =
	    makeApiCall(dispatcher) {
	    supabaseClient.auth.signInWith(OTP) {
	        this.email = email
	    }
	}
	// ...
}

So far, we have created an interface for the repository and implemented the signIn() function using Supabase as the backend. We now want to use the function inside our ViewModels. For this to work, we will use Koin Dependency Injection.

We first declare a Koin module, which is the implementation details we want to inject. We use single because we only need one instance of it.

val repositoryModule = module {
    single<UserRepository> {
        SupabaseUserRepositoryImpl()
    }
}

Now we are ready to use (inject) the implementation we defined earlier. To use it in a ViewModel, we can add the repo in class the parameter like so:

class UserLoginViewModel(
    private val UserRepository: UserRepository
) : SomeBaseViewModel {
	// ...
}

This is a ViewModel that we will also use Koin to inject for us. So we make a ViewModel module. We use viewModel as opposed to single so that Koin knows to match this injectable to the lifecycle of Android ViewModels (thereby preventing memory leaks and other issues). The get() function automatically finds what the UserLoginViewModel class needs and injects the SupabaseUserRepositoryImpl singleton we defined above.

val viewModelModule = module {
    viewModel {
        UserLoginViewModel(get())
    }
}

Finally, in the entry point to our Compose application, we start the Koin Application and pass in the above modules.

@Composable
fun App() {
    KoinApplication(
        application = {
            modules(
            	listOf(
            	    viewModelModule,
            	    repositoryModule
            	)
            )
        }
    ) {
        // App code goes here
    }
}

Now private val UserRepository: UserRepository is injected and ready to be used in UserLoginViewModel.

Next Week

  • Beautify and improve the login screen (e.g., error state warning, loading icons, etc).
  • Implement input validation.
  • Handle exceptions (e.g., wrong email, wrong OTP, user not found, etc).
  • Handle user registration.
  • Handle session renewal.
  • Use proper string resources as opposed to hard coding text.