L’authentification est une étape essentielle dans de nombreuses applications, c’est pourquoi c’est une des premières fonctionnalités que l’on peut vouloir mettre en place. Pour faire cela, on pense naturellement à utiliser les services Firebase qui offrent une solution puissante pour l’authentification des utilisateurs. Mais qu’en est-il lorsque l’on développe une application mobile avec Kotlin Multiplatform ?
Pour rappel Kotlin Multiplatform, aussi appelé KMP (et non plus KMM), est un framework qui permet de partager du code entre différentes plateformes et notamment entre Android et iOS. Il favorise la réutilisation de la logique métier tout en gardant le développement des interfaces natif. Cela permet donc de garder les avantages et les performances du natif tout en bénéficiant des avantages du code partagé.
Pendant mes recherches je me suis aperçue que Firebase ne gère pas nativement l’intégration de ses services avec des applications KMP. Cependant, il existe une bibliothèque créée et maintenue par GitLive du nom de Firebase Kotlin SDK, que l’on peut retrouver sur le site officiel Firebase Open Source.
L’API de Firebase Kotlin SDK est similaire à celle de Firebase Android SDK Kotlin Extensions mais supporte aussi les projets multiplateformes, ce qui permet d’utiliser Firebase directement dans le code partagé entre les plateformes iOS, Android, Desktop ou Web.
Cependant, avant d’adopter cette bibliothèque, il faut savoir si elle correspond à votre besoin, notamment en prenant en compte que cette dernière est toujours en cours de développement et que certains services Firebase ne sont pas encore complètement supportés, comme vous pouvez le voir sur ce tableau récapitulatif de la couverture d’API proposé par Firebase SDK Kotlin.
Dans cet article, nous explorerons comment j’ai intégré et utilisé les services d'authentification de Firebase dans une application KMP avec Firebase Kotlin SDK dans un de mes projets à travers l’ajout d’une authentification en anonyme mais aussi avec le SignIn de Google.
Mise en place de l’environnement
Créer un projet KMP
Commençons par ajouter le plugin Kotlin Multiplatform Mobile disponible dans Préférences/Plugins sur Android Studio ou bien sur le site de Jetbrains.
Une fois le plugin installé, on peut créer un nouveau projet en choisissant Kotlin Multiplatform App. Pour cette démonstration, je nomme le projet KMP Firebase Integration et choisis de mettre com.ippon.kmp_firebase_integration
en tant que namespace de l’application. J'ai choisi de garder le nom par défaut des dossiers.
Créer un projet Firebase
Nous aurons aussi besoin de créer un nouveau projet sur Firebase afin de pouvoir intégrer ses services dans notre application. Pour cela, allez dans la console de Firebase et cliquez sur “Ajouter un projet”. Étant donné que nous utilisons une technologie multiplateformes, il faut ajouter deux applications distinctes : une application Android et une application iOS.
Pour l’application Android :
La première étape pour ajouter une application Android sur Firebase est de fournir le nom du package de l’application. Ce qui correspond à l’applicationId
que l’on peut retrouver dans le build.gradle
. Attention cependant, comme nous sommes ici dans une application KMP, il faut aller chercher l’applicationId
qui se trouve dans androidApp/build.gradle.kts
, c’est-à-dire, dans notre cas com.ippon.kmp_firebase_integration.android
.
Ensuite, nous aurons aussi besoin d’un certificat de signature de débogage SHA-1 car nous souhaitons utiliser les services d’authentification de Google. Pour cela, plusieurs méthodes existent mais pour des questions de simplicité, je vais utiliser la commande signingReport
de Gradle dans le terminal d’Android Studio.
./gradlew signingReport
Cela va me générer des informations de signature pour chaque variant de mon application. Voilà les informations qui m'intéressent concernant androidApp
:
Une fois ces informations rentrées, cliquez sur “Enregistrer l’application”.
Téléchargez le fichier google-services.json
.
Copiez ce fichier dans le dossier androidApp
.
Pour rendre visible le contenu de notre google-services.json
nous devons ajouter le plugin Gradle des services Google. Pour cela, il suffit d’ajouter dans le fichier Gradle à la racine du projet kmp-firebase-integration/build.gradle.kts
:
plugins {
// ...
id("com.google.gms.google-services") version "4.4.0" apply false
}
Ensuite, dans le fichier androidApp/build.gradle.kts
, on ajoute le plugin des services Google et les SDK Firebase que nous aurons besoin, c’est-à-dire celui de l’authentification :
plugins {
id("com.android.application")
// Ajouter le plugin Gradle des services Google.
id("com.google.gms.google-services")
}
dependencies {
// Importer le BoM Firebase
implementation(platform("com.google.firebase:firebase-bom:32.6.0"))
// Ajouter les dépendances Firebase nécessaires.
implementation("com.google.firebase:firebase-auth")
}
Pour l’application iOS :
La première étape pour ajouter une application iOS sur Firebase est de fournir l’ID du bundle Apple. Ouvrez votre projet avec XCode, sélectionnez le projet dans le navigateur de projet à gauche. Sélectionnez ensuite Targets/Général. L’identifiant du bundle se trouve sous Identity, dans notre cas on a com.ippon.kmp-firebase-integration
.
Une fois ces informations rentrées, cliquez sur “Enregistrer l’application”.
Téléchargez le fichier GoogleService-Info.plist
.
Ensuite, il faut ajouter le SDK Firebase au projet XCode, pour cela accéder à Fichier > Ajouter des packages
. Dans la barre de recherche, copiez ce lien https://github.com/firebase/firebase-ios-sdk, sélectionnez le package proposé et cliquez sur “Ajouter”.
Une fois le téléchargement fini, une pop-up s'affiche pour demander de choisir les bibliothèques Firebase dont nous aurons besoin, c’est-à-dire FirebaseAuth
pour l’authentification.
Configurer Firebase
Pour pouvoir utiliser l’authentification dans notre projet, nous devons tout d’abord l’initialiser dans la console Firebase. Dans l’onglet à gauche de la console, aller dans Créer > Authentification
, et cliquer sur le bouton “Commencer”. On doit ensuite choisir les méthodes d’authentification que nous souhaitons intégrer à notre projet.
Pour ce projet, nous choisirons l’authentification avec un compte anonyme, il suffit simplement de cliquer sur le toggle pour activer cette méthode.
Nous ajouterons aussi l’authentification avec un compte Google. Lorsque nous cliquons sur le toggle “Activer” nous devons rentrer le nom public du projet.
Après cette configuration, Firebase nous informe que nous devons mettre à jour les fichiers google-services.json
et GoogleService-Info.plist
.
Implémentation de l’authentification
Authentification avec un compte anonyme
Common
Entrons maintenant dans le vif du sujet en commençant par implémenter l’authentification avec un compte anonyme sur nos applications.
Tout d’abord, dans le build.gradle.kts
du module shared, ajouter dans kotlin > sourceset > commonMain.dependencies
, la dépendance à la bibliothèque Firebase SDK Kotlin qui nous servira à appeler l’API de Firebase directement dans notre code partagé.
commonMain.dependencies {
// Firebase SDK Kotlin
implementation("dev.gitlive:firebase-auth:1.10.4")
}
Une fois la synchronisation faite, on peut commencer à créer les fonctions dont nous avons besoin. Pour cela, on peut, dans un premier temps, aller dans shared > src > commonMain > kotlin > com.ippon.kmp_firebase_integration
et créer un nouveau module que l’on peut nommer service et qui contiendra nos appels aux services de Firebase.
Dans ce module service, nous allons créer une interface AuthenticationInterface dans laquelle on peut déjà ajouter une fonction pour l’authentification anonyme.
suspend fun signInAnonymously()
Ensuite, on crée une classe AuthenticationService
qui prend en paramètre l’initialisation de FirebaseAuth afin de rendre notre classe plus facilement testable en donnant la possibilité de mocker ce paramètre. Elle hérite donc de notre interface et contient l’implémentation de notre fonction de connexion anonyme.
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.auth.FirebaseAuth
import dev.gitlive.firebase.auth.auth
class AuthenticationService(private val firebaseAuth: FirebaseAuth = Firebase.auth
) : AuthenticationInterface {
override suspend fun signInAnonymously() {
firebaseAuth.signInAnonymously()
}
}
Enfin, on peut créer un nouveau module viewmodel, qui contiendra comme son nom l’indique, notre AuthenticationViewModel
. De la même façon, il prendra en paramètre une instance de AuthenticationService
afin de pouvoir appeler la fonction d’authentification anonyme.
import com.ippon.kmp_firebase_integration.service.AuthenticationInterface
import com.ippon.kmp_firebase_integration.service.AuthenticationService
class AuthenticationViewModel constructor(
private val authenticationService: AuthenticationInterface = AuthenticationService(),
) {
constructor() : this(AuthenticationService())
private val coroutineScope: CoroutineScope = MainScope()
fun signInAnonymously() {
coroutineScope.launch {
authenticationService.signInAnonymously()
}
}
}
Il ne reste donc plus qu’à appeler cette fonction dans nos views Android et iOS.
Android
Premièrement, dans androidApp > src > main > AndroidManifest.xml
, ajoutez la permission d’utiliser internet <uses-permission android:name="android.permission.INTERNET"/>
.
Créer une nouvelle fonction AuthenticationView
avec l’annotation @Composable qui représente la page d’authentification et dans laquelle on ajoute un bouton “Continuer en invité”. Lorsque l’on clique dessus, cela va déclencher l’appel de la fonction signInAnonymously
de notre shared ViewModel dans le LaunchEffect
car c’est une fonction asynchrone.
@Composable
fun AuthenticationView(
authenticationViewModel: AuthenticationViewModel
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Button(
onClick = { authenticationViewModel.signInAnonymously()},
modifier = Modifier
.padding(12.dp),
shape = RoundedCornerShape(12.dp),
) {
Text(text = "Continuer en invité")
}
}
}
Dans le MainActivity
, on ajoute la création de l’instance de l’AuthenticationViewModel
que l’on passe en paramètre de notre AuthenticationView
. Si on lance ce code on obtient cette vue :
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
val authenticationViewModel = AuthenticationViewModel()
AuthenticationView(authenticationViewModel)
}
}
}
}
}
iOS
Effacez le contenu du ContentView
, pour y ajouter une variable qui crée une instance de notre ViewModel partagé AuthentificationViewModel
. Dans son body, on ajoute aussi un bouton qui aura comme label “Continuer en invité” et qui aura comme action d’appeler la fonction signInAnonymously
provenant de l'AuthenticationViewModel
du module shared. Comme c’est une fonction asynchrone, on doit précéder notre appel par le mot clé await et entourer notre fonction dans une Task et un try/catch.
import SwiftUI
import shared
struct ContentView: View {
var authenticationViewModel: AuthenticationViewModel = AuthenticationViewModel()
var body: some View {
VStack(alignment: .center) {
Button(action: {
authenticationViewModel.signInAnonymously()
}) {
Text("Continuer en invité")
}
}
}
}
import SwiftUI
@main
struct iOSApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Nous devons aussi mettre à jour l’AppDelegate
afin d’y ajouter le module Firebase.
import Foundation
import UIKit
import Firebase
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
return true
}
}
Résultat
Et si l’on clique sur notre bouton “Continuer en invité” sur notre application Android ou iOS, on remarque dans la console de Firebase une nouvelle connexion anonyme, ce qui prouve le fonctionnement de notre authentification sur les deux plateformes 🎉.
Authentification avec un compte Google
Pour effectuer cette authentification, nous aurons besoin de récupérer un idToken et un accessToken de la part de Google et cela ne peut s’effectuer qu’en natif Android et iOS. Voici une représentation schématique du flow:
Common
Pour commencer, retournons dans notre code partagé et dans le build.gradle.kts
ajoutez la dépendance suivante:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
Ajoutons ensuite dans notre interface la signature de notre fonction qui servira à appeler le service Google et Firebase. Notre fonction prend en paramètres deux tokens, un idToken et un accessToken qui nous serviront à transmettre les crédits Google à Firebase.
suspend fun signInWithGoogle(idToken: String?, accessToken: String?)
On ajoute ensuite son implémentation dans AuthenticationService
, où l’on fait appel au GoogleAuthProvider
en lui donnant en paramètre nos deux tokens que l’on va récupérer par la suite. Ainsi, on obtient notre credential Google que l’on peut donc donner à la fonction signInWithCredential
de Firebase dans le but de compléter notre authentification.
import dev.gitlive.firebase.auth.GoogleAuthProvider
override suspend fun signInWithGoogle(idToken: String?, accessToken: String?) {
val credential = GoogleAuthProvider.credential(idToken, accessToken)
firebaseAuth.signInWithCredential(credential)
}
N’oublions pas de mettre à jour notre ViewModel:
fun signInWithGoogle(idToken: String?, accessToken: String?) {
coroutineScope.launch {
authenticationService.signInWithGoogle(idToken = idToken, accessToken = accessToken)
}
}
On cherche maintenant à récupérer nos tokens Google afin de pouvoir les passer en paramètre de notre fonction d’authentification.
Android
Pour créer notre client d’authentification Google, nous devons ajouter cette dépendance dans notre build.gradle.kts
:
// Google authentication
implementation("com.google.android.gms:play-services-auth:20.4.1")
Ensuite, nous devons créer une variable que nous appellerons gso
qui contiendra notre GoogleSignInOptions et sa configuration. On utilisera la configuration par défaut pour l’authentification avec Google avec l’option GoogleSignInOptions.DEFAULT_SIGN_IN
.
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken("86184358628-u14g08iq6mnje0qo76ke6tg0l69k75k4.apps.googleusercontent.com")
.requestEmail()
.build()
Pour ce qui est du paramètre de requestIdToken, nous aurons besoin de récupérer l’ID client de notre projet, nous pouvons le retrouver, soit dans notre google-services.json
ou bien dans la console Google Cloud de notre projet.
Puis, dans notre AuthenticationView
récupérer le contexte en tant qu’Activity
afin de passer cette variable activity
et notre gso
en tant que paramètre de la fonction GoogleSignIn.getClient
afin de pouvoir créer notre client Google.
val activity = LocalContext.current as Activity
var googleSignInClient = GoogleSignIn.getClient(activity, gso)
Une fois que l’on a fini ces configurations, on va pouvoir ajouter un launcher
pour lancer une nouvelle activité qui permettra à l’application de lancer la pop-up Google qui permet à l’utilisateur de choisir l’adresse mail Google avec laquelle il souhaite se connecter.
var googleIdToken by rememberSaveable { mutableStateOf("") }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
try {
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
// Google Sign In was successful, authenticate with Firebase
val account = task.getResult(ApiException::class.java)!!
authenticationViewModel.signInWithGoogle(account.idToken.toString(), null)
} catch (e: ApiException) {
// Google Sign In failed, update UI appropriately
Log.w("TAG", "Google sign in failed", e)
}
}
Une fois cette étape réalisée, on récupère le résultat qui représente les informations du compte Google sélectionné, mais ce qui nous intéresse ici c’est d’extraire le idToken. Que l’on va stocker dans une variable afin de pouvoir la passer en paramètre de notre fonction d’authentification.
On peut créer un nouveau Composable pour contenir notre button Google personnalisé.
@Composable
fun GoogleSignInButton(
onClick: () -> Unit
) {
Button(
onClick = onClick,
shape = RoundedCornerShape(12.dp),
border = BorderStroke(1.dp, Color.LightGray),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
),
) {
Image(
painter = painterResource(id = R.drawable.ic_google_logo),
contentDescription = "Logo Google"
)
GoogleSignInText()
}
}
@Composable
fun GoogleSignInText() {
Text(
text = "Continuer avec Google",
color = Color.Gray,
fontSize = 14.sp,
modifier = Modifier.padding(4.dp),
)
}
Et l’ajouter dans notre AuthenticationView
en intégrant dans le onClick
l’appel qui lancera notre launcher
.
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Button(
onClick = { authenticationViewModel.signInAnonymously() },
modifier = Modifier
.padding(12.dp),
shape = RoundedCornerShape(12.dp),
) {
Text(text = "Continuer en invité")
}
GoogleSignInButton(onClick = {
val signInIntent = googleSignInClient.signInIntent
launcher.launch(signInIntent)
})
}
iOS
Pour commencer, nous devons ajouter une nouvelle dépendances dans notre projet Xcode, en allant dans File > Add Packages…
et en cherchant dans la barre de recherche https://github.com/google/GoogleSignIn-iOS, ne pas oublier d’ajouter aussi l’extension GoogleSignInSwift
car nous utilisons SwiftUI.
Dans Info.plist
ajoutez les informations concernant l’identifiant de notre client iOS:
<key>GIDClientID</key>
<string>YOUR_IOS_CLIENT_ID</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>YOUR_DOT_REVERSED_IOS_CLIENT_ID</string>
</array>
</dict>
</array>
Remplacer les id clients par ceux correspondant, que l’on retrouve dans GoogleService-Info
.
On peut maintenant passer au code, en commençant par ajouter l’import qui nous servira à appeler la fonction de récupération de token Google:
import GoogleSignIn
Ensuite, on crée une fonction pour gérer nos appels, dans laquelle on va stocker dans la variable presentingViewController
notre rootViewController
afin de pouvoir le passer en paramètre de la méthode GIDSignIn.sharedInstance.signIn
. Puis récupérer les tokens qui nous intéressent dans signInResult.user
, c’est-à-dire, idToken et accessToken, que l’on passe donc en paramètre de notre fonction d’authentification.
func handleSignInButton(presentingViewController: UIViewController?) {
if (presentingViewController == nil) {
return
}
GIDSignIn.sharedInstance.signIn(withPresenting: presentingViewController!) { signInResult, error in
guard error == nil else { return }
guard let signInResult = signInResult else { return }
signInResult.user.refreshTokensIfNeeded { user, error in
guard error == nil else { return }
guard let user = user else { return }
let idToken = user.idToken
let accessToken = user.accessToken
authenticationViewModel.signInWithGoogle(idToken: idToken?.tokenString, accessToken: accessToken.tokenString)
}
}
}
On créer une struct pour contenir notre button Google personnalisé.
struct CustomGoogleButton: View {
var action: (() -> Void)?
var body: some View {
ZStack(){
Button{
self.action?()
}label: {
HStack {
Image("IcGoogleLogo")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 20, height: 20)
.scaledToFit()
.clipped()
Text("Continuer avec Google")
.padding()
}.frame(height: 20)
.padding()
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.gray, lineWidth: 1)
)
}
}
}
}
Enfin, on ajoute dans notre body notre button Google en lui donnant en paramètre la méthode qu’il appellera lors du clique.
var body: some View {
VStack(alignment: .center) {
Button(action: {
authenticationViewModel.signInAnonymously()
}) {
Text("Continuer en invité")
}
.onOpenURL { url in
GIDSignIn.sharedInstance.handle(url)
}
CustomGoogleButton(action: {
let presentingViewController = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first?.rootViewController
handleSignInButton(presentingViewController: presentingViewController)
})
}
}
Résultat
Pour constater le bon fonctionnement de notre authentification, on peut se rendre sur la console de Firebase dans l’onglet Authentification et voir que l’on s’est bien connecté avec un compte anonyme et avec un compte Google 🎉.
Conclusion
Voici donc une approche envisageable si vous souhaitez intégrer les services Firebase dans une application KMP grâce à la bibliothèque Firebase Kotlin SDK. Il est possible que d'autres solutions émergent, voire qu'un support officiel de la part de Firebase soit proposé (sait-on jamais) au vue de la communauté très active autour de ce nouveau framework prometteur qu'est KMP.
Vous pouvez retrouver le projet GitLab ici.