1. 事前準備
瞭解如何使用 Credential Manager 在 Android 上實作「使用 Google 帳戶登入」功能。
必要條件
課程內容
- 建立 Google Cloud 專案和 OAuth 用戶端。
- 實作底部功能表登入流程。
- 實作明確的按鈕登入流程。
需求條件
- 已安裝 Android Studio。
- 符合 Android Studio 和模擬器系統需求的電腦。
- 已安裝 Java Development Kit (JDK)。
2. 建立 Android Studio 專案
如要開始使用,請在 Android Studio 中建立新專案:
- 開啟 Android Studio,然後按一下「New Project」。

- 依序選取「Phone and Tablet」>「Empty Activity」,然後點選「Next」。

- 設定專案設定:
- 名稱:選擇專案名稱。
- 套件名稱:使用預設名稱或選擇自訂名稱。
- SDK 最低版本:選取最新的穩定版或 > Android 14。

- 按一下「Finish」,然後等待初始專案建構完成。

3. 設定 Google Cloud 專案
建立 Google Cloud 專案
- 前往 Google Cloud 控制台,然後選取或建立專案。

- 依序前往「APIs & Services」(API 和服務) >「OAuth consent screen」(OAuth 同意畫面)。

- 按一下「開始使用」,然後填寫必填欄位:
- 應用程式名稱:使用 Android 應用程式的名稱。
- 「使用者支援電子郵件」:選取您的 Google 帳戶。
- 目標對象:選取「外部」。
- 聯絡資訊:輸入電子郵件地址。

- 詳閱《Google API 服務:使用者資料政策》,然後按一下「建立」。

設定 OAuth 用戶端
您需要在 Google Cloud 控制台中建立網頁用戶端和 Android 用戶端,才能取得驗證用的用戶端 ID。
- Android 用戶端:驗證應用程式的套件名稱和 SHA-1 簽章,確保要求安全無虞。
- 網路用戶端:做為 Google 登入服務的後端用戶端。
建立 Android OAuth 2.0 用戶端
- 在「用戶端」頁面中,按一下「建立用戶端」,然後選取「Android」做為「應用程式類型」。

- 輸入應用程式的套件名稱 (與
MainActivity.kt的第 1 行相符)。 - 產生 SHA-1 簽章。開啟 Android Studio 終端機並執行下列指令:macOS/Linux:
Windows:keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
keytool -list -v -keystore "C:\Users\USERNAME\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android
- 從指令輸出內容複製 SHA-1 指紋,貼到控制台的「SHA-1 指紋」欄位,然後按一下「建立」。

建立網頁 OAuth 2.0 用戶端
- 再次點選「建立用戶端」,然後選取「網頁應用程式」做為「應用程式類型」。
- 為網路用戶端命名,將「網址/來源」欄位留空,然後按一下「建立」。

- 從確認對話方塊中複製產生的「用戶端 ID」。您會在 Kotlin 程式碼中使用此 ID。

4. 設定 Android 虛擬裝置
如要測試應用程式,可以使用 Android 實體裝置或 Android 虛擬裝置 (AVD)。
建立並執行 AVD
- 在 Android Studio 中開啟「裝置管理工具」,按一下「Create Virtual Device」 (或「+」圖示),然後選取「Medium Phone」。
- 選取最新的穩定版做為系統映像檔,然後按一下「完成」。
- 按一下裝置旁邊的「Play/Run」(播放/執行) 圖示,啟動模擬器。

在裝置上登入 Google 帳戶
- 在模擬器上開啟「設定」應用程式,然後前往「Google」。
- 按一下「登入 Google 帳戶」,然後按照提示操作。

5. 新增依附元件
將驗證和 Google ID 整合所需的程式庫新增至專案:
- 依序前往「File」>「Project Structure」>「Dependencies」>「app」。
- 依序點選「+」>「程式庫依附元件」,搜尋
com.google.android.libraries.identity.googleid:googleid,然後選取最新版本 (例如1.1.1)。 - 再次按一下「+」>「Library Dependency」,搜尋
play-services-auth,然後選取 Group ID 為com.google.android.gms的程式庫。 - 按一下「確定」即可套用變更並同步處理專案。

6. 實作底部功能表流程

底部功能表流程會運用 Credential Manager API,為使用者提供簡化的方式,讓他們在 Android 裝置上使用 Google 帳戶登入應用程式。這個流程專為快速和便利性而設計,特別是針對回訪使用者,應在應用程式啟動時觸發。
建立登入要求
- 如要開始使用,請開啟
MainActivity.kt並移除預設的Greeting()和GreetingPreview()函式。 - 在第 3 行開始的現有匯入陳述式後,新增下列匯入陳述式:
import android.content.Context import android.os.Build import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.credentials.CredentialManager import androidx.credentials.CustomCredential import androidx.credentials.GetCredentialRequest import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.credentials.exceptions.GetCredentialCustomException import androidx.credentials.exceptions.GetCredentialException import androidx.credentials.exceptions.NoCredentialException import com.google.android.libraries.identity.googleid.GetGoogleIdOption import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import java.security.SecureRandom import java.util.Base64 import kotlinx.coroutines.delay import kotlinx.coroutines.launch const val TAG = "MainActivity" - 在
MainActivity.kt檔案的MainActivity類別下方,新增這個可組合函式:@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @Composable fun BottomSheet(webClientId: String) { val context = LocalContext.current // LaunchedEffect is used to run a suspend function when the composable is first launched. LaunchedEffect(Unit) { // Create a Google ID option with filtering by authorized accounts enabled. val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder() .setFilterByAuthorizedAccounts(true) .setServerClientId(webClientId) .setNonce(generateSecureRandomNonce()) .build() // Create a credential request with the Google ID option. val request: GetCredentialRequest = GetCredentialRequest.Builder() .addCredentialOption(googleIdOption) .build() // Attempt to sign in with the created request using an authorized account val e = signIn(request, context) // If the sign-in fails with NoCredentialException, there are no authorized accounts. // In this case, we attempt to sign in again with filtering disabled. if (e is NoCredentialException) { val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder() .setFilterByAuthorizedAccounts(false) .setServerClientId(webClientId) .setNonce(generateSecureRandomNonce()) .build() val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder() .addCredentialOption(googleIdOptionFalse) .build() //We will build out this function in a moment signIn(requestFalse, context) } } } //This function is used to generate a secure nonce to pass in with our request fun generateSecureRandomNonce(byteLength: Int = 32): String { val randomBytes = ByteArray(byteLength) SecureRandom.getInstanceStrong().nextBytes(randomBytes) return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes) }
程式碼細目
LaunchedEffect(Unit):在首次顯示 Composable 時立即觸發登入流程。GetGoogleIdOption.Builder():設定 Google ID 權杖要求。setFilterByAuthorizedAccounts(true):首先,系統會篩選使用者已授權給這個應用程式的帳戶,嘗試進行無聲登入,盡量減少回訪者的登入阻力。setNonce(...):傳遞generateSecureRandomNonce()為每項要求產生的安全隨機 Nonce,防止重送攻擊。
signIn(request, context):執行要求。如果失敗並傳回NoCredentialException(表示沒有先前授權的帳戶),流程會回溯至setFilterByAuthorizedAccounts(false),讓使用者從裝置上登入的任何 Google 帳戶中選取。
發出登入要求
建構登入要求後,您可以使用 Credential Manager 完成登入程序。建立名為 signIn 的函式,執行要求並處理可能發生的常見例外狀況。
在 MainActivity.kt 檔案的 BottomSheet 函式下方新增這個函式:
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
val credentialManager = CredentialManager.create(context)
val failureMessage = "Sign in failed!"
//using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
//on the initial running of our app
delay(250)
return try {
// The getCredential is called to request a credential from Credential Manager.
val result = credentialManager.getCredential(
request = request,
context = context,
)
Log.i(TAG, result.toString())
val credential = result.credential
if (credential is CustomCredential &&
credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
Log.i(TAG, "Signed in as: ${googleIdTokenCredential.id}")
}
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
Log.i(TAG, "(☞゚ヮ゚)☞ Sign in Successful! ☜(゚ヮ゚☜)")
null
} catch (e: GoogleIdTokenParsingException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
e
} catch (e: NoCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": No credentials found", e)
e
} catch (e: GetCredentialCancellationException) {
Toast.makeText(context, "Sign-in cancelled", Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
e
} catch (e: GetCredentialCustomException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
e
} catch (e: GetCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Failure getting credentials", e)
e
}
}
程式碼細目
credentialManager.getCredential(...):呼叫 Credential Manager API,顯示系統帳戶選取器底部功能表或對話方塊。delay(250):暫停一小段時間,避免在 Credential Manager 服務完成初始化之前,於應用程式啟動時立即觸發底部功能表,導致競爭狀況。- 例外狀況處理:擷取並記錄常見的憑證錯誤 (例如取消、缺少憑證或權杖剖析問題),並使用快訊提供使用者意見回饋。
觸發底部功能表流程
更新 MainActivity 類別,在啟動時呼叫 BottomSheet()。將 YOUR_CLIENT_ID_HERE 替換為您的網頁應用程式用戶端 ID:
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
//This will trigger on launch
BottomSheet(webClientId)
}
}
}
}
}
儲存專案 (依序點選「File」>「Save」),然後執行應用程式:
- 按下執行按鈕:

- 應用程式在模擬器上啟動時,應該會顯示登入 Bottom Sheet。按一下「繼續」測試流程。

- 系統應會顯示浮動式訊息通知,確認登入成功。

7. 實作按鈕流程

按鈕流程會提供明確選項,供使用者登入或註冊。使用標準品牌宣傳可確保體驗一致。使用符合「使用 Google 帳戶登入」品牌宣傳指南的預先核准素材資源。
新增品牌圖示
- 請按這裡下載品牌素材資源,然後解壓縮 ZIP 檔案。
- 複製「
signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png」。 - 在 Android Studio 中,將檔案貼到「res」>「drawable」資料夾,將檔案重新命名為
siwg_button.png,然後按一下「OK」。
按鈕流程程式碼
這個流程會重複使用相同的 signIn 輔助函式,但會傳遞 GetSignInWithGoogleOption,而不是 GetGoogleIdOption。與底部功能表流程不同,明確按鈕流程不會預先篩選或自動提示儲存的憑證或密碼金鑰。將這個可組合函式貼到 BottomSheet 函式下方:
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val onClick: () -> Unit = {
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
.Builder(serverClientId = webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()
coroutineScope.launch {
signIn(request, context)
}
}
Image(
painter = painterResource(id = R.drawable.siwg_button),
contentDescription = "",
modifier = Modifier
.fillMaxSize()
.clickable(enabled = true, onClick = onClick)
)
}
程式碼細目
GetSignInWithGoogleOption:與 Bottom Sheet 流程不同,明確按鈕流程會使用這個選項提示使用者選取 Google 帳戶,不會自動篩選。coroutineScope.launch:啟動協同程式,在點選按鈕時非同步執行暫停signIn函式。Image:顯示品牌siwg_button可繪項目,並附加點擊事件監聽器來觸發流程。
將按鈕新增至 UI 版面配置
更新 MainActivity 版面配置,垂直對齊顯示自動 BottomSheet 和明確 ButtonUI 項目:
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//This will trigger on launch
BottomSheet(webClientId)
//This requires the user to press the button
ButtonUI(webClientId)
}
}
}
}
}
}
測試按鈕流程
- 執行應用程式。
- 按一下工作表區域外的位置,即可關閉初始的底部功能表。
- 按一下「使用 Google 帳戶登入」按鈕,啟動登入對話方塊,然後選取帳戶。

- 驗證結果:檢查 Logcat,確認使用者名稱/電子郵件地址是否已列印出來。
8. 結語
恭喜!您已成功使用 Android Credential Manager 實作「使用 Google 帳戶登入」功能。
其他資源
完整的 MainActivity.kt 程式碼
以下是 MainActivity.kt 的完整程式碼,供您參考:
package com.example.example
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.example.example.ui.theme.ExampleTheme
import android.content.ContentValues.TAG
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.credentials.exceptions.GetCredentialException
import androidx.compose.foundation.clickable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.credentials.CredentialManager
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialCustomException
import androidx.credentials.exceptions.NoCredentialException
import androidx.credentials.GetCredentialRequest
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import java.security.SecureRandom
import java.util.Base64
import kotlinx.coroutines.CoroutineScope
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//This will trigger on launch
BottomSheet(webClientId)
//This requires the user to press the button
ButtonUI(webClientId)
}
}
}
}
}
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun BottomSheet(webClientId: String) {
val context = LocalContext.current
// LaunchedEffect is used to run a suspend function when the composable is first launched.
LaunchedEffect(Unit) {
// Create a Google ID option with filtering by authorized accounts enabled.
val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
// Create a credential request with the Google ID option.
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()
// Attempt to sign in with the created request using an authorized account
val e = signIn(request, context)
// If the sign-in fails with NoCredentialException, there are no authorized accounts.
// In this case, we attempt to sign in again with filtering disabled.
if (e is NoCredentialException) {
val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOptionFalse)
.build()
signIn(requestFalse, context)
}
}
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val onClick: () -> Unit = {
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
.Builder(serverClientId = webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()
coroutineScope.launch {
signIn(request, context)
}
}
Image(
painter = painterResource(id = R.drawable.siwg_button),
contentDescription = "",
modifier = Modifier
.fillMaxSize()
.clickable(onClick = onClick)
)
}
fun generateSecureRandomNonce(byteLength: Int = 32): String {
val randomBytes = ByteArray(byteLength)
SecureRandom.getInstanceStrong().nextBytes(randomBytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
val credentialManager = CredentialManager.create(context)
val failureMessage = "Sign in failed!"
//using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
//on the initial running of our app
delay(250)
return try {
// The getCredential is called to request a credential from Credential Manager.
val result = credentialManager.getCredential(
request = request,
context = context,
)
Log.i(TAG, result.toString())
val credential = result.credential
if (credential is CustomCredential &&
credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
Log.i(TAG, "Signed in as: ${googleIdTokenCredential.id}")
}
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
Log.i(TAG, "(☞゚ヮ゚)☞ Sign in Successful! ☜(゚ヮ゚☜)")
null
} catch (e: GoogleIdTokenParsingException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
e
} catch (e: NoCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": No credentials found", e)
e
} catch (e: GetCredentialCancellationException) {
Toast.makeText(context, "Sign-in cancelled", Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
e
} catch (e: GetCredentialCustomException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
e
} catch (e: GetCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Failure getting credentials", e)
e
}
}