JetpackでGoogleログインからAPIコール 【コピペOK!】
Jetpack Compose を使って Google Drive API v3 のメソッドを実行する方法を解説します
例では Google Drive の API を使用しますが、Spreadsheet API なども API コール以外は同様の手順が必要になります
やりたいこと
Jetpack Compose で Google ログイン(サインイン)画面を表示したい!
そして Google Drive API v3 を呼びたい!
準備
Google Cloud Platform 設定
コーディングの前に Google Cloud Platform の設定が必要になります
前回の記事で紹介しているためこちらを参考にしてください
重要なのは、認証情報は Android 用とウェブ用の両方を設定し、ウェブ用のクライアント ID を使用する点です
Dependency 設定
下記の Dependency を Build.gradle(.kts) に設定してください
また下記のバージョンでしか試してないため、ご了承ください
dependencies {
implementation("com.google.api-client:google-api-client-android:2.2.0") {
exclude("org.apache.httpcomponents")
}
implementation("com.google.apis:google-api-services-drive:v3-rev136-1.25.0") {
exclude("org.apache.httpcomponents")
}
implementation("com.google.http-client:google-http-client-gson:1.42.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.8.0-RC2")
implementation("com.google.android.gms:play-services-auth:20.7.0")
}
エラーの解消
同期後にビルドをすると「2 files found with path ‘META-INF/DEPENDENCIES’」エラーが表示されると思います
これを解消するために、app モジュールにある build.gradle(.kts) の resources に「excludes += “/META-INF/DEPENDENCIES”」を追加します
// build.gradle(.kts)(:app)
android {
...
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" // 既存
excludes += "/META-INF/DEPENDENCIES" // 追加
}
}
}
※app以外のモジュールに追加してもエラーは解消されません
実装
ここからは実装方法を解説しています
コードは問題なく動きますが、全体的に実装途中のため、DataSource に ViewModel から直接アクセスしてたりと色々おかしな部分がありますがご容赦ください
各ファイルの全体像は、それぞれのファイル名タイトルの下にあります
MVVM に部分的に沿った構成になっているため、MainActivity、GoogleSignIn、MainActivityViewModel の3つのファイルに分かれています
アーキテクチャに関しては Android 公式サイトを参考にしてください
MainActivity.kt
package your.package.name // ご自身のものに変更してください
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity: ComponentActivity() {
private val viewModel: MainActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
YourAppTheme { // ご自身のアプリのものに変更してください
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column {
Test(
viewModel.credential.value,
viewModel::deleteAllTrash
)
ShowDialog(viewModel.intent.value)
GoogleSignIn(
serverClientId = "your_client_ID_for_web",// ご自身のウェブ用クライアント ID に変更してください
onSuccessListener = viewModel::saveLoginSuccess,
onFailureListener = viewModel::setLoginFailure,
content = {
Text(text = "Google sign in")
}
)
}
}
}
}
}
}
@Composable
fun Test(googleSignInState: GoogleSignInState, op: (GoogleAccountCredential) -> Unit) {
val displayText = when (googleSignInState) {
is GoogleSignInState.ActivityNotFoundException -> "activity exception"
is GoogleSignInState.ApiException -> "api exception"
is GoogleSignInState.TaskFailed -> "task failed"
is GoogleSignInState.SendIntentException -> "intent exception"
is GoogleSignInState.Success -> "succeeded"
is GoogleSignInState.Loading -> "loading"
}
Text(
text = displayText
)
if (googleSignInState is GoogleSignInState.Success) {
op(googleSignInState.credential)
}
}
@Composable
fun ShowDialog(userRecoverableAuthIntentSate: UserRecoverableAuthIntentSate) {
when (userRecoverableAuthIntentSate) {
is UserRecoverableAuthIntentSate.Failed -> {
val context = LocalContext.current
LaunchedEffect(key1 = Unit) {
context.startActivity(
userRecoverableAuthIntentSate.e
)
}
}
else -> {}
}
}
MainActivity の重要な点は、42行目のクライアント ID です
こちらは「準備」パートで取得したウェブ用のクライアント ID を指定してください
41行目から48行目で Google サインインを行っています
82行目から86行目は、 アプリ実行後に指定した Google アカウントとこのアプリの連携が初めての場合に実行されます
MainActivityViewModel.kt
package your.package.name // ご自身のものに変更してください
import android.accounts.Account
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.gms.common.api.ApiException
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.gson.GsonFactory
import com.google.api.services.drive.Drive
import com.google.api.services.drive.DriveScopes
import dagger.assisted.AssistedFactory
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.launch
import java.util.Collections
import javax.inject.Inject
@HiltViewModel
@SuppressLint("StaticFieldLeak")
class MainActivityViewModel @Inject constructor(
@ApplicationContext private val context: Context,
): ViewModel() {
var credential: MutableState<GoogleSignInState> = mutableStateOf(GoogleSignInState.Loading)
private set
var intent: MutableState<UserRecoverableAuthIntentSate> =
mutableStateOf(UserRecoverableAuthIntentSate.Loading)
private set
private val tag = "MainActivityViewModel"
@Inject lateinit var googleDriveDataSourceFactory: GoogleDriveDataSourceFactory
private fun setupDataSource(credential: GoogleAccountCredential): GoogleDriveDataSource {
val service = Drive.Builder(
NetHttpTransport(),
GsonFactory.getDefaultInstance(),
credential
)
.setApplicationName("Bill Splitter")
.build()
return googleDriveDataSourceFactory.create(service)
}
fun deleteAllTrash(credential: GoogleAccountCredential) {
viewModelScope.launch {
try {
val googleDriveDataSource = setupDataSource(credential)
googleDriveDataSource.deleteAllTrash()
intent.value = UserRecoverableAuthIntentSate.Success
} catch (e: Exception) {
Log.e(tag, e.toString())
if (e is UserRecoverableAuthIOException) {
intent.value = UserRecoverableAuthIntentSate.Failed(e.intent)
}
}
}
}
fun saveLoginSuccess(credentialId: String) {
val packageName = "your.package.name" // ご自身のものに変更してください
val account = Account(credentialId, packageName)
val googleAccountCredential =
GoogleAccountCredential
.usingOAuth2(
context,
Collections.singleton(DriveScopes.DRIVE)
)
.setSelectedAccount(account)
this.credential.value = GoogleSignInState.Success(googleAccountCredential)
}
fun setLoginFailure(e: Exception) {
credential.value = when (e) {
is ActivityNotFoundException -> {
GoogleSignInState.ActivityNotFoundException(e)
}
is ApiException -> {
GoogleSignInState.ApiException(e)
}
is IntentSender.SendIntentException -> {
GoogleSignInState.SendIntentException(e)
}
else -> {
GoogleSignInState.TaskFailed(e)
}
}
}
}
sealed interface GoogleSignInState {
data class ActivityNotFoundException(val e: android.content.ActivityNotFoundException): GoogleSignInState
data class ApiException(val e: com.google.android.gms.common.api.ApiException): GoogleSignInState
data class TaskFailed(val e: Exception): GoogleSignInState
data class SendIntentException(val e: IntentSender.SendIntentException): GoogleSignInState
data class Success(val credential: GoogleAccountCredential): GoogleSignInState
data object Loading: GoogleSignInState
}
sealed interface UserRecoverableAuthIntentSate {
data class Failed(val e: Intent): UserRecoverableAuthIntentSate
data object Success: UserRecoverableAuthIntentSate
data object Loading: UserRecoverableAuthIntentSate
}
@AssistedFactory
interface GoogleDriveDataSourceFactory {
fun create(driveService: Drive): GoogleDriveDataSource
}
情報量が多いですが、重要な部分は下記の3点です
・44行目の setupDataSource()
・56行目の deleteAllTrash()
・71行目の saveLoginSuccess()
この中でも特に重要なのは saveLoginSuccess() です
setupDataSource()
こちらの目的は Hilt の Assisted Injection パターンを使い、GoogleDriveDataSource クラスを DI する際に動的な値をコンストラクタに渡すことです
※Assisted Injection パターンについてはこちら
※GoogleDriveDataSource クラスについては下で解説
この関数の注目点は、動的に渡している Google Drive API v3 の Drive クラスです
Drive クラスのインスタンス化を Builder クラスで行っており、setupDataSource() のパラメーターとして設定している credential を48行目で使用しています
deleteAllTrash()
※後日更新予定
saveLoginSuccess()
※後日更新予定
実現したいことは、受け取った credentialId を使用して GoogleAccountCredential クラスを作成することです
GoogleAccountCredential は Google アカウントの認証と選択を管理するクラスです
GoogleSignIn.kt
package your.package.name // ご自身のものに変更してください
import android.content.ActivityNotFoundException
import android.content.IntentSender
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.Button
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.google.android.gms.auth.api.identity.BeginSignInRequest
import com.google.android.gms.auth.api.identity.Identity
import com.google.android.gms.common.api.ApiException
@Composable
fun GoogleSignIn(
modifier: Modifier = Modifier,
serverClientId: String,
onSuccessListener: (String) -> Unit,
onFailureListener: (Exception) -> Unit,
content: @Composable (RowScope.() -> Unit)
) {
val context = LocalContext.current
val oneTapClient = Identity.getSignInClient(context)
val signInRequest =
BeginSignInRequest
.builder()
.setGoogleIdTokenRequestOptions(
BeginSignInRequest
.GoogleIdTokenRequestOptions
.builder()
.setSupported(true)
.setServerClientId(serverClientId)
.setFilterByAuthorizedAccounts(false)
.build()
)
.setAutoSelectEnabled(true)
.build()
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
) {
try {
val oneTapCredential = oneTapClient.getSignInCredentialFromIntent(it.data)
onSuccessListener(oneTapCredential.id)
} catch (e: ApiException) {
onFailureListener(e)
}
}
val startGoogleLogin = {
oneTapClient
.beginSignIn(signInRequest)
.addOnSuccessListener { result ->
try {
launcher.launch(
IntentSenderRequest.Builder(result.pendingIntent.intentSender).build()
)
} catch (e: IntentSender.SendIntentException) {
onFailureListener(e)
} catch (e: ActivityNotFoundException) {
onFailureListener(e)
}
}
.addOnFailureListener { e ->
onFailureListener(e)
}
}
Button(
onClick = {
startGoogleLogin()
},
modifier = modifier,
content = content,
)
}
このコンポーザブル関数の目的は、認証情報の取得をすることです
42行目から51行目で宣言している launcher を58行目の launch() で実行することにより、ViewModel に認証情報を渡しています
取得の全体的な流れは公式ドキュメントで推奨されている方法に沿っています
launcher の宣言
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
) {
try {
val oneTapCredential = oneTapClient.getSignInCredentialFromIntent(it.data)
onSuccessListener(oneTapCredential.id)
} catch (e: ApiException) {
onFailureListener(e)
}
}
この変数宣言の目的は、activity の開始をリクエストすることと、リクエスト実施後の結果を用いて行う作業内容を記述することです
サインイン API クライアント(サインイン API を操作する側)
val context = LocalContext.current
val oneTapClient = Identity.getSignInClient(context)
val signInRequest =
サインイン API の開始ポイントである Identity クラスを使用し、SignInClient クラスを取得しています
val startGoogleLogin = {
oneTapClient
.beginSignIn(signInRequest)
.addOnSuccessListener { result ->
try {
launcher.launch(
IntentSenderRequest.Builder(result.pendingIntent).build()
)
} catch (e: IntentSender.SendIntentException) {
onFailureListener(e)
} catch (e: ActivityNotFoundException) {
onFailureListener(e)
}
}
.addOnFailureListener { e ->
onFailureListener(e)
}
}
SignInClient.beginSignIn() でサインインを開始し、結果を保持する Task を受け取ります
Task の成功時に activity を実行する(ローンチする)のが目的です
55行目の signInRequest についてはこちらです
BeginSignInRequest インスタンスの作成
val signInRequest =
BeginSignInRequest
.builder()
.setGoogleIdTokenRequestOptions(
BeginSignInRequest
.GoogleIdTokenRequestOptions
.builder()
.setSupported(true)
.setServerClientId(serverClientId)
.setFilterByAuthorizedAccounts(false)
.build()
)
.setAutoSelectEnabled(true)
.build()
サインインフロー開始時に設定するオプションを作成しています
.setGoogleIdTokenRequestOptions(
BeginSignInRequest
.GoogleIdTokenRequestOptions
.builder()
.setSupported(true)
.setServerClientId(serverClientId)
.setFilterByAuthorizedAccounts(false)
.build()
)
サインイン実行中の設定や選択できるアカウントの制限などを設定しています
GoogleDriveDataSource.kt
package your.package.name // ご自身のものに変更してください
import com.google.api.services.drive.Drive
import your.package.name.GoogleDrive // ご自身のものに変更してください
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class GoogleDriveDataSource @AssistedInject constructor(
@Assisted private val driveService: Drive,
): BstGoogleDrive {
suspend fun deleteAllTrash() {
withContext(Dispatchers.IO) {
driveService.files().emptyTrash().execute()
}
}
}
※ViewModel から DataSource への直アクセスは推奨されないため、Repository の作成をお勧めします
Drive API v3 を使用して、Google Drive のごみ箱からすべてのアイテムを消去しています
エラーの解消
上記の ViewModel では既に対応済みですが、アプリが Google Drive に対して行う予定の操作を、サインインした Google アカウントが承認済みでない場合、下記エラーが出力されます
// Logcat など
[GoogleAuthUtil] isUserRecoverableError status: NEED_REMOTE_CONSENT
com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException
上記を例にすると、MainActivityViewModel.kt の78行目で DriveScopes.DRIVE を指定し、このアプリが Google Drive のファイル全般に対して操作することを表しています
この操作について、サインインしている Google アカウントに対して確認をとるため、上記エラーが出力されます
対処は MainActivity.kt の ShowDialog() で行っています
エラーとして返される Intent を使用して activity を実行しています
@Composable
fun ShowDialog(userRecoverableAuthIntentSate: UserRecoverableAuthIntentSate) {
when (userRecoverableAuthIntentSate) {
is UserRecoverableAuthIntentSate.Failed -> {
val context = LocalContext.current
LaunchedEffect(key1 = Unit) {
context.startActivity(
userRecoverableAuthIntentSate.e
)
}
}
else -> {}
}
}
おわりに
不明瞭なところなどが多々あると思いますので、定期的に更新しようと思います
また、コード自体も突貫工事感が強いので、修正後に何らかの形で載せようと思います
他にもご要望などあればお気軽にコメントまたはContactからご連絡ください