جریان ورود با Google Identity Services و Firebase
اکثر برنامه های اندروید دارای نوعی احراز هویت هستند. برای این پست، نحوه عملکرد این جریان با استفاده از ورود به سیستم با یک ضربه، Firebase و Amity گوگل را خواهیم دید.
پشته فناوری که ما استفاده خواهیم کرد این است:
- اسکریپت کاتلین (KTS) برای Gradle ما
- Jetpack Compose برای رابط کاربری ما
- معماری MVVM
- هیلت برای تزریق وابستگی
- SDK اجتماعی Amity
- احراز هویت OneTap گوگل
- احراز هویت Firebase
جریان به شرح زیر است: ابتدا، بررسی می کنیم که آیا جلسه Amity معتبر است یا خیر. اگر اینطور باشد، ما به جریان اصلی خود ادامه خواهیم داد، در غیر این صورت، کاربران خود را به صفحه ورود به سیستم هدایت خواهیم کرد. در آنجا، ابتدا سعی می کنیم کاربران خود را با استفاده از گوگل وارد کنیم. اگر قبلاً به برنامه ما وارد شده باشند، این کار با موفقیت انجام می شود و سپس می توانیم با استفاده از Firebase وارد شوید. اگر نه، ابتدا باید کاربران خود را در برنامه خود ثبت نام کنیم و سپس با Firebase ادامه دهیم.
پیکربندی
خوب، بیایید شروع کنیم! ابتدا برای پیکربندی پروژه خود از راهنمای رسمی استفاده می کنیم. از آنجایی که ما از Groovy استفاده نمی کنیم، وابستگی ها مطابق شکل زیر اضافه می شوند.
plugins {
...
id("com.google.gms.google-services") version "4.3.14"
}
dependencies {
...
implementation("com.google.android.gms:play-services-auth:20.4.0")
}
apply(plugin = "com.google.gms.google-services")
*در حالی که باید از فایل libs.versions ارائه شده (کاتالوگ نسخه ها) استفاده کنید، برای خوانایی کد در این پست، نسخه هایی مانند این را اضافه می کنیم.
از آنجایی که اینجا هستیم، وابستگیهای Amity & Firebase را نیز اضافه میکنیم که بعداً به آن نیاز خواهیم داشت. متأسفانه، افزودن وابستگیهای Firebase در قالب هنوز از طریق دستیار Android Studio و بدون استثنا امکانپذیر نیست، بنابراین ما آنها را به صورت دستی اضافه میکنیم.
dependencies {
...
// Amity
implementation("com.github.AmityCo.Amity-Social-Cloud-SDK-Android:amity-sdk:5.33.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.6.4")
// Firebase
implementation(platform("com.google.firebase:firebase-bom:31.1.1"))
implementation("com.google.firebase:firebase-analytics-ktx")
implementation("com.google.firebase:firebase-auth-ktx")
implementation("com.google.android.gms:play-services-auth:20.4.0")
implementation("com.google.firebase:firebase-firestore-ktx")
}
آخرین مرحله ما پیکربندی کنسول Firebase است. طبق اسناد رسمی:
برای استفاده از یک ارائه دهنده احراز هویت، باید آن را در کنسول Firebase فعال کنید. به صفحه Sign-in Method در بخش Firebase Authentication بروید تا ورود ایمیل/گذرواژه و سایر ارائه دهندگان هویتی را که می خواهید برای برنامه خود فعال کنید.
البته ما ورود به سیستم Google را فعال خواهیم کرد. سپس به تنظیمات پروژه می رویم و اثر انگشت گواهی SHA خود را اضافه می کنیم.
اکنون ما آماده ایم تا کد ورود خود را اضافه کنیم! ابتدا فایل های مورد نیاز خود را ایجاد می کنیم: MainNavigation
، MainViewModel
، LoginScreen
، LoginViewModel
، AuthRepository
.
مشاهده وضعیت احراز هویت
اولین Composable ما که نامیده می شود MainNavigation
. برای بررسی مداوم اعتبار جلسه Amity، اینجاست که وضعیت آن را بررسی می کنیم. از آنجایی که می خواهیم وضعیت برنامه ما وضعیت جلسه را منعکس کند، آن را به a نگاشت می کنیم StateFlow
.
StateFlow
یک جریان قابل مشاهده توسط دارنده حالت است که به روز رسانی های وضعیت فعلی و جدید را به جمع کننده های خود منتشر می کند. وضعیت فعلیvalue
همچنین می توان از طریق ویژگی ارزش آن خواند. در اندروید،StateFlow
برای کلاس هایی که نیاز به حفظ حالت قابل مشاهده قابل مشاهده دارند مناسب است.
ما شروع می کنیم و دریافت می کنیم SessionState
در AuthRepository
، سپس در ما MainViewModel
ما جریان خود را به a تبدیل می کنیم StateFlow
، و در نهایت در ما MainNavigation
ما آن را مشاهده می کنیم.
override val amitySession = flow {
emit(AmityCoreClient.currentSessionState)
AmityCoreClient.observeSessionState().asFlow()
}
val uiState: StateFlow<MainUiState> = authRepository
.amitySession.map {
when(it) {
SessionState.NotLoggedIn,
SessionState.Establishing -> MainUiState.LoggedOut
SessionState.Established,
SessionState.TokenExpired -> MainUiState.LoggedIn
is SessionState.Terminated -> MainUiState.Banned
}
}
.catch { Error(it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MainUiState.Loading)LaunchedEffect(lifecycleOwner) {
viewModel.uiState.collect { state ->
when(state) {
MainUiState.Banned -> {} //TODO snackbar
is MainUiState.Error -> {} //TODO snackbar
MainUiState.Loading -> { /* no-op */ }
MainUiState.LoggedIn -> navController.navigate("main") { popUpTo(0) }
MainUiState.LoggedOut -> navController.navigate("login") { popUpTo(0) }
}
}
}
LaunchedEffect(lifecycleOwner) {
viewModel.uiState.collect { state ->
when(state) {
MainUiState.Banned -> showSnackbar(scope, snackbarHostState, userBannedText)
is MainUiState.Error -> showSnackbar(scope, snackbarHostState, userErrorText)
MainUiState.Loading -> { /* no-op */ }
MainUiState.LoggedIn -> navController.navigate(Route.UsersList.route) { popUpTo(0) }
MainUiState.LoggedOut -> navController.navigate(Route.Login.route) { popUpTo(0) }
}
}
}
جالب است، اکنون می توانیم وضعیت کاربران خود را مشاهده کنیم! 🎉🎉🎉
ورود به سیستم Google OneTap
کاربران ما اکنون صفحه ورود ✨ درخشان✨ ما را می بینند که واقعاً در حال حاضر فقط به شرح زیر است
@Composable
fun LoginScreen(
modifier: Modifier = Modifier,
navigateToUsers: () -> Unit,
viewModel: LoginViewModel = hiltViewModel()
) {
Column(modifier) {
Button(onClick = { /* TODO */ }) {
Text(text = stringResource(R.string.login_google_bt))
}
}
}
(صفحه ورود ما)
بالاخره اونقدر هم براق نیست😛
برای شروع، ما به تماس ورود به سیستم خود نیاز داریم AuthRepository
. *
ما از یک رابط برای کپسوله کردن خود استفاده می کنیم AuthRepository
توابع این به طور خاص برای ایجاد یک ماکت مفید خواهد بود AuthRepository
برای آزمایشات ما در آینده
ما signInRequest
، SignUpRequest
و همچنین Firebase و مشتریان Google در اختیار ما قرار می گیرند AuthRepository
پیاده سازی در طول تزریق وابستگی همانطور که در زیر نشان داده شده است.
@Module
@InstallIn(SingletonComponent::class)
class AuthModule {
private val signInRequest = BeginSignInRequest.builder()
.setGoogleIdTokenRequestOptions(
BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
.setSupported(true)
.setServerClientId(BuildConfig.SERVER_CLIENT_ID)
// Only show accounts previously used to sign in.
.setFilterByAuthorizedAccounts(true)
.build())
// Automatically sign in when exactly one credential is retrieved.
.setAutoSelectEnabled(true)
.build()
private val signUpRequest = BeginSignInRequest.builder()
.setGoogleIdTokenRequestOptions(
BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
.setSupported(true)
.setServerClientId(BuildConfig.SERVER_CLIENT_ID)
.setFilterByAuthorizedAccounts(false)
.build())
.build()
@Provides
@Singleton
fun provideAuthRepository(@ApplicationContext appContext: Context) : AuthRepository {
return AuthRepositoryImp(
Identity.getSignInClient(appContext),
signInRequest,
signUpRequest,
Firebase.auth,
Firebase.firestore
)
}
}
بازگشت به جریان ورود به سیستم! همانطور که در بالا ذکر شد، اگر اولین بار است که کاربر ما سعی می کند با این حساب وارد سیستم شود، این یک استثنا ایجاد می کند. برای جلوگیری از نمایش پیامهای خطای نادرست به کاربر، وقتی در روش ورود به سیستم خود استثناء دریافت میکنیم، سعی میکنیم ثبت نام کنیم. اگر موفقیت آمیز بود، ادامه می دهیم، اگر نه، خطا را مدیریت می کنیم.
//AuthRepositoryImpl.kt
override suspend fun signInWithGoogle(): OneTapResponse {
return try {
val result = oneTapClient.beginSignIn(signInRequest).await()
ApiResponse.Success(result)
} catch (e: Exception) {
ApiResponse.Failure(e)
}
}
override suspend fun signUpWithGoogle(): OneTapResponse {
return try {
val result = oneTapClient.beginSignIn(signUpRequest).await()
ApiResponse.Success(result)
} catch (e: Exception) {
ApiResponse.Failure(e)
}
}
//LoginViewModel
suspend fun googleSignIn(launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>) {
when (val oneTapResponse: ApiResponse<BeginSignInResult> =
authRepository.signInWithGoogle()) {
is ApiResponse.Success -> {
val result = oneTapResponse.data!!
val intent = IntentSenderRequest.Builder(result.pendingIntent.intentSender).build()
launcher.launch(intent)
}
is ApiResponse.Loading -> { /* no-op */ }
else -> {
// No saved credentials found. Launch the One Tap sign-up flow
googleSignUp(launcher)
}
}
}
private suspend fun googleSignUp(launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>) {
when(val oneTapResponse: ApiResponse<BeginSignInResult> = authRepository.signUpWithGoogle()) {
is ApiResponse.Success -> {
val result = oneTapResponse.data!!
val intent = IntentSenderRequest.Builder(result.pendingIntent.intentSender).build()
launcher.launch(intent)
}
is ApiResponse.Loading -> { /* no-op */ }
else -> handleSignUpError()
}
}
اگر به جای Compose از Views استفاده می کردیم، نتیجه intent را در خود مدیریت می کردیم onActivityForResult
روش. در عوض، ما از ManagedActivityResultLauncher در خود استفاده خواهیم کرد LoginScreen
. اگر راهانداز ما با نتیجه مثبت بازگردد، شناسه کاربر خود را دریافت میکنیم و وارد Firebase میشویم.
@Composable
fun LoginScreen(
modifier: Modifier = Modifier,
viewModel: LoginViewModel = hiltViewModel()
) {
val launcher = rememberFirebaseAuthLauncher(viewModel = viewModel)
val scope = rememberCoroutineScope()
Column(modifier) {
Button(onClick = { scope.launch { viewModel.googleSignIn(launcher) } }) {
Text(text = stringResource(R.string.login_google_bt))
}
}
}
@Composable
private fun rememberFirebaseAuthLauncher(viewModel: LoginViewModel): ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult> {
val scope = rememberCoroutineScope()
val context = LocalContext.current
return rememberLauncherForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
result.data.let {
try {
scope.launch {
val credentials =
Identity.getSignInClient(context).getSignInCredentialFromIntent(result.data)
val googleIdToken = credentials.googleIdToken
val googleCredentials = getCredential(googleIdToken, null)
// TO-DO sign-in to Firebase
}
} catch (e: Exception) {
Log.e("LOG", e.message.toString())
// TO-DO show error to user.
}
}
}
}
ورود به سیستم Firebase و Amity
اوف، تقریبا تمام شد!!!
بازگشت به ما AuthRepository
ابتدا تماس های خود را برای Firebase و Amity اضافه می کنیم.
override suspend fun firebaseSignInWithGoogle(googleCredential: AuthCredential): SignInToFirebaseResponse {
return try {
val authResult = auth.signInWithCredential(googleCredential).await()
val isNewUser = authResult.additionalUserInfo?.isNewUser ?: false
if (isNewUser) {
addUserToFirestore()
}
ApiResponse.Success(true)
} catch (e: Exception) {
ApiResponse.Failure(e)
}
}
override suspend fun amityLogIn() = login(userId = currentUserId)
.build()
.submit()
.toSuspend()
اگر کاربر جدید است، ما همچنین آن را به Firestore اضافه می کنیم که برای برنامه چت ما لازم است، اما این اجباری نیست. سپس در ما LoginViewModel
ما توابع تعلیق خود را می نامیم:
fun firebaseSignIn(authCredential: AuthCredential) {
viewModelScope.launch {
authRepository.firebaseSignInWithGoogle(authCredential)
.also { authRepository.amityLogIn() }
}
}
و TO-DO ما را با این فراخوان در لانچر ما جایگزین کنید.
در حال دور شدن از ورود ما
… یا مشاهده وضعیت فرآیند ورود به سیستم. آخرین قدم ما، قول می دهم!
از خودمان شروع می کنیم AuthRepository
دوباره، و تماس نهایی خود را اضافه کنید.
override val isSignedIn = callbackFlow {
val authStateListener = FirebaseAuth.AuthStateListener { auth ->
trySend(auth.currentUser != null)
}
auth.addAuthStateListener(authStateListener)
awaitClose {
auth.removeAuthStateListener(authStateListener)
}
}
از آنجایی که این یک callback است، به این معنی است که هر بار که مقدار auth.currentUser تغییر کند، به ما اطلاع داده می شود. سپس در ما LoginViewModel
ما آن را به یک وضعیت UI نگاشت می کنیم و البته آن را به حالت خود مشاهده می کنیم LoginScreen
.
// LoginViewModel
val uiState: StateFlow<LoginUiState> = authRepository
.isSignedIn.map {if (it) LoginUiState.Authorized else LoginUiState.Unauthorized()}
.catch { LoginUiState.Unauthorized(it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), LoginUiState.Loading)
// LoginScreen
val authStatus by produceState<LoginUiState>(
initialValue = LoginUiState.Unauthorized(),
key1 = lifecycle,
key2 = viewModel.uiState
) {
lifecycle.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
viewModel.uiState.collect { value = it }
}
}
(مسیرهای شاد و … نه چندان خوشحال ما. رسیدگی به خطاها در هنگام ورود به سیستم بسیار مهم است!)
اووووووو این همه مردمی!
اگر به نحوه استفاده از این کد برای یک برنامه چت علاقه مند هستید، حتما پست بعدی وبلاگ ما را بررسی کنید!
کد کامل را می توانید در اینجا پیدا کنید.