Kotlin + Jetpack Compose では、生体認証そのものは Jetpack Biometric ライブラリを使用し、Compose から呼び出す形が一般的です。
まず依存関係を追加します。
dependencies {
implementation("androidx.biometric:biometric:1.2.0-alpha05")
}
生体認証マネージャークラス
再利用しやすいようにクラス化すると次のようになります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
package com.example.biometric import androidx.activity.ComponentActivity import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat class BiometricAuthManager( private val activity: ComponentActivity ) { fun isBiometricAvailable(): Boolean { val biometricManager = BiometricManager.from(activity) return biometricManager.canAuthenticate( BiometricManager.Authenticators.BIOMETRIC_STRONG ) == BiometricManager.BIOMETRIC_SUCCESS } fun authenticate( title: String = "生体認証", subtitle: String = "本人確認を行います", onSuccess: () -> Unit, onError: (String) -> Unit ) { val executor = ContextCompat.getMainExecutor(activity) val biometricPrompt = BiometricPrompt( activity, executor, object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded( result: BiometricPrompt.AuthenticationResult ) { super.onAuthenticationSucceeded(result) onSuccess() } override fun onAuthenticationError( errorCode: Int, errString: CharSequence ) { super.onAuthenticationError(errorCode, errString) onError(errString.toString()) } override fun onAuthenticationFailed() { super.onAuthenticationFailed() onError("認証に失敗しました") } } ) val promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle(title) .setSubtitle(subtitle) .setNegativeButtonText("キャンセル") .build() biometricPrompt.authenticate(promptInfo) } } |
Composeから利用
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@Composable fun LoginScreen() { val activity = LocalContext.current as ComponentActivity val biometricManager = remember { BiometricAuthManager(activity) } Button( onClick = { biometricManager.authenticate( onSuccess = { println("認証成功") }, onError = { println("認証失敗: $it") } ) } ) { Text("生体認証でログイン") } } |
AndroidManifest
特別な権限は通常不要ですが、機能宣言を追加できます。
<uses-feature
android:name="android.hardware.fingerprint"
android:required="false" />
ViewModelと組み合わせる場合
Composeでは認証結果をStateFlowに流す構成がよく使われます。
sealed interface AuthState {
data object Idle : AuthState
data object Success : AuthState
data class Error(val message: String) : AuthState
}
class AuthViewModel : ViewModel() {
private val _authState =
MutableStateFlow<AuthState>(AuthState.Idle)
val authState = _authState.asStateFlow()
fun onAuthSuccess() {
_authState.value = AuthState.Success
}
fun onAuthError(message: String) {
_authState.value = AuthState.Error(message)
}
}
Compose側では collectAsState() で監視します。
この構成にすると、認証処理は BiometricAuthManager、状態管理は ViewModel、UIはComposeという責務分離ができ、保守しやすくなります。
SwiftUIのように自動で認証画面は出ない
Jetpack Compose + Android の BiometricPrompt は、SwiftUI の LocalAuthentication のように画面表示まで自動では行いません。
iOSでは例えば:
let context = LAContext()
context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "ログインします"
) { success, error in
...
}
この1行の呼び出しで Face ID / Touch ID ダイアログが表示されます。
一方 Android では:
biometricPrompt.authenticate(promptInfo)
を呼んだ瞬間にシステムの生体認証ダイアログが表示されますが、その前に
BiometricPromptPromptInfoAuthenticationCallback
を作成する必要があります。
つまり Android は
BiometricPrompt作成
↓
PromptInfo作成
↓
authenticate()
↓
システム認証画面表示
という流れです。
SwiftUI に近い使い勝手にしたい場合は、自分でラッパークラスを作ります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class BiometricAuthenticator( private val activity: ComponentActivity ) { suspend fun authenticate(): Boolean = suspendCancellableCoroutine { cont -> val prompt = BiometricPrompt( activity, ContextCompat.getMainExecutor(activity), object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded( result: BiometricPrompt.AuthenticationResult ) { cont.resume(true) } override fun onAuthenticationError( errorCode: Int, errString: CharSequence ) { cont.resume(false) } } ) prompt.authenticate( BiometricPrompt.PromptInfo.Builder() .setTitle("認証") .setNegativeButtonText("キャンセル") .build() ) } } |
Compose側では
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
val scope = rememberCoroutineScope() Button( onClick = { scope.launch { val success = authenticator.authenticate() if (success) { // ログイン } } } ) { Text("認証") } |
と書けるので、SwiftUI の
let success = await authenticate()
にかなり近い感覚になります。
ちなみに Android 14以降でも、Composeに「Face IDボタンを置いたら自動で認証UIが出る」ような仕組みはなく、必ず BiometricPrompt.authenticate() を呼び出してシステムダイアログを表示します。これはセキュリティ上、アプリが勝手に認証画面を出し続けることを防ぐためでもあります。