JetpackComposeでFirebaseAuth(パスワード認証)
FirebaseAuthのパスワード認証をJetpackComposeで実装したいと思います。
The Firebase Blogの「Adding Firebase Authentication to a Jetpack Compose app」を参考にしたのですが...いかんせんわかりづらいですね...
もう少しかみ砕いた感じで実装していきます。
関連記事
- JetpackComposeでFirebaseAuth(GoogleOneTap認証)
- JetpackComposeでFirebaseAuth(Google認証)
- JetpackComposeでFirebaseAuth(メールリンク認証)
FirebaseAuth(パスワード認証, 匿名ユーザ)のエンドポイント
エンドポイント | |
---|---|
auth.signInAnonymously | 匿名ユーザの作成とログイン |
auth.signInWithEmailAndPassword | パスワード認証ユーザのログイン |
auth.createUserWithEmailAndPassword | パスワード認証ユーザの作成 |
auth.addAuthStateListener | 認証情報が変化した時のリスナーを登録 |
auth.removeAuthStateListener | 認証情報が変化した時のリスナーを削除 |
auth.currentUser | ユーザ情報 |
匿名ユーザとパスワード認証のユーザで、作成・ログインを行う場合に大体使用するのはこのあたりになります。
ユーザ情報は、signInAnonymously()などの操作系関数の戻り値でも取得できますし、
auth.currentUserの状態変化時に発火するリスナー経由でも取得できます。
- 匿名ユーザの作成シーケンス
- auth.addAuthStateListener()で、auth.currentUserの状態変化時に発火するリスナーを登録する
- Firebase.auth.signInAnonymously()を呼ぶ
- パスワード認証ユーザの作成シーケンス
- auth.addAuthStateListener()で、auth.currentUserの状態変化時に発火するリスナーを登録する
- Firebase.auth.createUserWithEmailAndPassword(email, password)を呼ぶ
- パスワード認証ユーザのログインシーケンス
- auth.addAuthStateListener()で、auth.currentUserの状態変化時に発火するリスナーを登録する
- Firebase.auth.signInWithEmailAndPassword(email, password)を呼ぶ
FirebaseAuthの導入
Compose、Firebase(google-services.jsonの配置とか)、Hiltの導入は完了している前提です。
まずは、gradleに依存関係を入れていきます。
// firebase
implementation platform('com.google.firebase:firebase-bom:32.2.0')
+ implementation 'com.google.firebase:firebase-auth-ktx'
モジュール構成
モジュール構成が気になる方は展開してください
ドメイン層
クラス名 | タイプ | 定義 |
---|---|---|
Account | data class | FirebaseUserクラスの情報を表現します |
AccountFuture<out Account> | sealed class | Accountの状態(認証済orまだ)を表現します |
AccountRepository | interface | Accountの作成、ログインを行います |
インフラストラクチャ層
クラス名 | タイプ | 定義 |
---|---|---|
AccountRepositoryImpl | class | AccountRepositoryの実態 |
ユースケース(application)層
クラス名 | タイプ | 定義 |
---|---|---|
SigninUsecase | class | ユーザがAuthに対してログインする |
SignupUsecase | class | ユーザがAuthに対して新規作成する |
プレゼンテーション層
クラス名 | タイプ | 定義 |
---|---|---|
SigninScreen | class | ログイン・新規作成を行う画面 |
SigninViewModel | class | SigninScreenのViewModel |
SigninUiState | class | SigninScreenのUiStateモデル |
AccountRepositoryImpl(インフラ層)の実装
クラスの定義
interface AccountRepository { /* ... */ }
class AccountRepositoryImpl @Inject constructor(
private val auth: FirebaseAuth,
) : AccountRepository {
/* ... */
}
DI
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
fun auth(): FirebaseAuth = Firebase.auth
@Provides
fun provideAccountRepository(auth: FirebaseAuth): AccountRepository = AccountRepositoryImpl(auth)
}
実装
interface AccountRepository {
+ val accountFuture: Flow<AccountFuture<Account>>
+ suspend fun createAnonymousAccount()
+ suspend fun signin(email: String, password: String)
+ suspend fun signup(email: String, password: String)
}
class AccountRepositoryImpl @Inject constructor(
private val auth: FirebaseAuth,
) : AccountRepository {
+ override val accountFuture: Flow<AccountFuture<Account>>
+ get() = callbackFlow {
+ val listener = FirebaseAuth.AuthStateListener { auth ->
+ this.trySend(AccountFuture.fromFirebaseUser(auth.currentUser))
+ }
+ auth.addAuthStateListener(listener)
+ awaitClose { auth.removeAuthStateListener(listener) }
+ }
+ override suspend fun createAnonymousAccount() {
+ auth.signInAnonymously().await()
+ }
+ override suspend fun signin(email: String, password: String) {
+ auth.signInWithEmailAndPassword(email, password).await()
+ }
+ override suspend fun signup(email: String, password: String) {
+ val currentUser = auth.currentUser
+ if (currentUser != null && currentUser.isAnonymous) {
+ val credential = EmailAuthProvider.getCredential(email, password)
+ currentUser.linkWithCredential(credential).await()
+ } else {
+ auth.createUserWithEmailAndPassword(email, password).await()
+ }
+ }
}
関数 | 役割 |
---|---|
accountFuture | addAuthStateListenerに追加したリスナーが発火したときにcallbackするためのcallbackFlow |
createAnonymousAccount | 匿名ユーザの作成を行う |
signin | パスワード認証ユーザのログインを行う |
signup | 匿名ユーザでログイン中の場合は、そのアカウントをパスワード認証として紐付ける。 それ以外は、パスワード認証のユーザを作成する |
ユースケース層(SignupUsecase,SigninUsecase)の実装
あまりにもちっさいクラスなので、Interfaceは作ってないです。
それぞれRepositoryの対応する関数を呼ぶだけで、認証情報はコールバックを利用して取得しようと思います。
class SigninUsecase @Inject constructor(
private val accountRepository: AccountRepository,
) {
suspend fun signin(email: String, password: String) {
accountRepository.signin(email, password)
}
}
class SignupUsecase @Inject constructor(
private val accountRepository: AccountRepository,
) {
suspend fun signup(email: String, password: String) {
accountRepository.signup(email, password)
}
}
ViewModel(SigninViewModel)の実装
1. uiStateのStateを作ります。
@HiltViewModel
class SigninViewModel : ViewModel() {
private val _uiState: MutableState<SigninUiState> = mutableStateOf(SigninUiState.initial)
val uiState: State<SigninUiState> = _uiState
}
2. 認証情報のcallbackFlowを受信できるようにします
@HiltViewModel
class SigninViewModel @Inject constructor(
+ accountRepository: AccountRepository,
) : ViewModel() {
private val _uiState: MutableState<SigninUiState> = mutableStateOf(SigninUiState.initial)
val uiState: State<SigninUiState> = _uiState
+ val accountState: StateFlow<AccountFuture<Account>> = accountRepository.accountFuture
+ .map {
+ _uiState.value = _uiState.value.updateCurrentUser(it)
+ it
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.Lazily,
+ initialValue = AccountFuture.Idle,
+ )
}
3. signinをScreenから呼べるようにします
@HiltViewModel
class SigninViewModel @Inject constructor(
+ private val signinUsecase: SigninUsecase,
accountRepository: AccountRepository,
) : ViewModel() {
/*...なんかいろいろ実装...*/
+ fun onSigninClick() {
+ val validatedMessage = _uiState.value.validateSignin()
+ if (validatedMessage != 0) {
+ SnackbarManager.showMessage(validatedMessage)
+ return
+ }
+
+ _uiState.value.doSignProcess()
+ viewModelScope.launch(
+ context = CoroutineExceptionHandler { /*coroutineContext*/_, throwable ->
+ SnackbarManager.showMessage(throwable.toSnackbarMessage())
+ },
+ block = {
+ signinUsecase.signin(_uiState.value.email, _uiState.value.password)
+ },
+ )
+ }
}
入力値は、TextFieldの変更に合わせて_uiStateの値を更新している前提です。
また、SnackbarManager.showMessageはこれをお手本にしてメッセージをSnackbarで表示します。
- validateSignin()で、signin用のvalidationを実行
- doSignProcess()で、UIを処理中モードに変更
- signinUsecase.signin()を呼び出し
※signin処理での例外発生時(パスワードが違うとか)は、CoroutineExceptionHandlerで処理されます。
4. signupをScreenから呼べるようにします
まぁSigninと同じですね。
@HiltViewModel
class SigninViewModel @Inject constructor(
private val signinUsecase: SigninUsecase,
+ private val signupUsecase: SignupUsecase,
accountRepository: AccountRepository,
) : ViewModel() {
/*...なんかいろいろ実装...*/
+ fun onSignupClick() {
+ val validatedMessage = _uiState.value.validateSignup()
+ if (validatedMessage != 0) {
+ SnackbarManager.showMessage(validatedMessage)
+ return
+ }
+
+ _uiState.value.doSignProcess()
+ viewModelScope.launch(
+ context = CoroutineExceptionHandler { /*coroutineContext*/_, throwable ->
+ SnackbarManager.showMessage(throwable.toSnackbarMessage())
+ },
+ block = {
+ signupUsecase.signup(_uiState.value.email, _uiState.value.password)
+ },
+ )
+ }
}
Screenの実装
@Composable
fun SigninScreen(
modifier: Modifier = Modifier,
viewModel: SigninViewModel = hiltViewModel(),
) {
viewModel.accountState.collectAsState()
val uiState by viewModel.uiState
/*...Composeとか実装...*/
}
「viewModel.accountState」は、StateFlowなので、collectAsState()を呼び出して状態を収集できるようにします。
Composeの定義などは、ご自由にどうぞ。
いざ実行
Signinをタップすると以下の画面になります(動画がうまく撮れなかった(>_<))
コードはこちらです。