LoginSignup
2
2

JetpackComposeでFirebaseAuth(パスワード認証)

Last updated at Posted at 2023-07-22

JetpackComposeでFirebaseAuth(パスワード認証)

FirebaseAuthのパスワード認証をJetpackComposeで実装したいと思います。
The Firebase Blogの「Adding Firebase Authentication to a Jetpack Compose app」を参考にしたのですが...いかんせんわかりづらいですね...
もう少しかみ砕いた感じで実装していきます。

関連記事

FirebaseAuth(パスワード認証, 匿名ユーザ)のエンドポイント

エンドポイント
auth.signInAnonymously 匿名ユーザの作成とログイン
auth.signInWithEmailAndPassword パスワード認証ユーザのログイン
auth.createUserWithEmailAndPassword パスワード認証ユーザの作成
auth.addAuthStateListener 認証情報が変化した時のリスナーを登録
auth.removeAuthStateListener 認証情報が変化した時のリスナーを削除
auth.currentUser  ユーザ情報

匿名ユーザとパスワード認証のユーザで、作成・ログインを行う場合に大体使用するのはこのあたりになります。
ユーザ情報は、signInAnonymously()などの操作系関数の戻り値でも取得できますし、
auth.currentUserの状態変化時に発火するリスナー経由でも取得できます。

  • 匿名ユーザの作成シーケンス
  1. auth.addAuthStateListener()で、auth.currentUserの状態変化時に発火するリスナーを登録する
  2. Firebase.auth.signInAnonymously()を呼ぶ
  • パスワード認証ユーザの作成シーケンス
  1. auth.addAuthStateListener()で、auth.currentUserの状態変化時に発火するリスナーを登録する
  2. Firebase.auth.createUserWithEmailAndPassword(email, password)を呼ぶ
  • パスワード認証ユーザのログインシーケンス
  1. auth.addAuthStateListener()で、auth.currentUserの状態変化時に発火するリスナーを登録する
  2. Firebase.auth.signInWithEmailAndPassword(email, password)を呼ぶ

FirebaseAuthの導入

Compose、Firebase(google-services.jsonの配置とか)、Hiltの導入は完了している前提です。
まずは、gradleに依存関係を入れていきます。

app/build.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で表示します。

  1. validateSignin()で、signin用のvalidationを実行
  2. doSignProcess()で、UIを処理中モードに変更
  3. 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をタップすると以下の画面になります(動画がうまく撮れなかった(>_<))
コードはこちらです。
signin.png

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2