안녕하세용! 안드로이드 앱에서 Firebase 를 활용하여 Google 로그인 하는 방법 및 소스코드 입니다.
1. Firebase Auth Google 설정 방법
https://eunoia3jy.tistory.com/129
1-1. Firebase 프로젝트 생성 및 앱 추가가 끝났다면 Firebase Console 에서 빌드 > Authentication 클릭
1-2. Sign-in method 탭에서 Google 에서 클릭
1-3. Google 을 사용설정으로 변경하고 프로젝트 지원 이메일 입력 후 저장
2. 소스 코드
LoginActivity.kt
internal class LoginActivity : AppCompatActivity() {
companion object {
const val TAG = "LoginActivity"
}
private val viewModel: LoginViewModel = LoginViewModel()
private lateinit var binding: ActivityLoginBinding //activity_login.xml 을 바인딩
private lateinit var fetchJob: Job
private var tokenId: String? = null //Google Auth 인증에 성공하면 token 값으로 설정된다
/* GoogleSignInOptions */
private val googleSignInOptions: GoogleSignInOptions by lazy {
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build()
}
/* GoogleSignIn */
private val googleSignIn by lazy {
GoogleSignIn.getClient(this, googleSignInOptions)
}
/* FirebaseAuth */
private val firebaseAuth by lazy {
FirebaseAuth.getInstance()
}
/* Google Auth 로그인 결과 수신 */
private val loginLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
Log.d(TAG, "loginLauncher - result : $result")
if (result.resultCode == Activity.RESULT_OK) {
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
try {
task.getResult(ApiException::class.java)?.let { account ->
Log.d(TAG, "loginLauncher - firebaseAuthWithGoogle : ${account.id}")
tokenId = account.idToken
viewModel.saveToken(
tokenId ?: throw java.lang.Exception()
) //Loading 상태 이후 Login 상태로 변경
} ?: throw Exception()
} catch (e: Exception) {
e.printStackTrace()
handleErrorState() //Error 상태
}
} else {
handleErrorState() //Error 상태
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root) //xml 전체를 감싸는 최상단 부모를 root 라는 property 로 제공
fetchJob = viewModel.fetchData(tokenId)
initViews()
observeData()
}
/* view 기본 설정 */
private fun initViews() = with(binding) {
tokenId?.let { //로그인 된 상태
groupLoginRequired.isGone = true
groupLogoutRequired.isVisible = true
} ?: kotlin.run { //로그인 안된 상태
groupLoginRequired.isVisible = true
groupLogoutRequired.isGone = true
}
btnLogin.setOnClickListener { //로그인 버튼 클릭 시
val signInIntent: Intent = googleSignIn.signInIntent
loginLauncher.launch(signInIntent) //loginLauncher 로 결과 수신하여 처리
}
btnLogout.setOnClickListener { //로그아웃 버튼 클릭 시
viewModel.signOut()
}
}
/* viewModel 을 관찰하여 상태 변화에 따라 처리 */
private fun observeData() = viewModel.loginStateLiveData.observe(this) {
Log.d(TAG, "observeData() - it : $it")
when (it) {
is LoginState.UnInitialized -> initViews()
is LoginState.Loading -> handleLoadingState()
is LoginState.Login -> handleLoginState(it)
is LoginState.Success -> handleSuccessState(it)
is LoginState.Error -> handleLoadingState()
}
}
/* Loading 상태인 경우 */
private fun handleLoadingState() = with(binding) {
progressBar.isVisible = true
groupLoginRequired.isGone = true
groupLogoutRequired.isGone = true
}
/* Google Auth Login 상태인 경우 */
private fun handleLoginState(state: LoginState.Login) = with(binding) {
progressBar.isVisible = true
val credential = GoogleAuthProvider.getCredential(state.idToken, null)
firebaseAuth.signInWithCredential(credential)
.addOnCompleteListener(this@LoginActivity) { task ->
if (task.isSuccessful) { //Login 성공
viewModel.setUserInfo(firebaseAuth.currentUser) //Login 상태 이후 Success 상태로 변경, 정보 설정
} else { //Login 실패
viewModel.setUserInfo(null)
}
}
}
/* Google Auth Login Success 상태인 경우 */
private fun handleSuccessState(state: LoginState.Success) = with(binding) {
progressBar.isGone = true
when (state) {
is LoginState.Success.Registered -> { //Google Auth 등록된 상태
handleRegisteredState(state) //Success.Registered 상태로 변경
}
is LoginState.Success.NotRegistered -> { //Google Auth 미등록된 상태
Toast.makeText(this@LoginActivity, "NotRegistered", Toast.LENGTH_SHORT).show()
groupLoginRequired.isVisible = true
groupLogoutRequired.isGone = true
}
}
}
/* Google Auth Login Registered 상태인 경우 */
private fun handleRegisteredState(state: LoginState.Success.Registered) = with(binding) {
groupLogoutRequired.isVisible = true
groupLoginRequired.isGone = true
Glide.with(this@LoginActivity)
.load(state.profileImgeUri.toString())
.transition(
DrawableTransitionOptions.withCrossFade(
DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build()
)
)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.apply {
transforms(CenterCrop(), RoundedCorners(60f.fromDpToPx()))
}
.into(ivProfile)
tvUsername.text = state.userName
}
/* Error 상태인 경우 */
private fun handleErrorState() = with(binding) {
Toast.makeText(this@LoginActivity, "Error State", Toast.LENGTH_SHORT).show()
}
}
activity_login.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".presentation.login.LoginActivity">
<androidx.constraintlayout.widget.Group
android:id="@+id/group_logout_required"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="btn_logout,tv_username,iv_profile"
tools:visibility="visible" />
<ImageView
android:id="@+id/iv_profile"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@color/black"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/iv_profile"
app:layout_constraintStart_toStartOf="@+id/iv_profile"
app:layout_constraintTop_toBottomOf="@+id/iv_profile"
tools:text="홍길동" />
<Button
android:id="@+id/btn_logout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="로그아웃"
app:layout_constraintBottom_toBottomOf="@+id/userNameTextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tv_username" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_login_required"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="visible"
app:constraint_referenced_ids="btn_login,tv_login_explain"
tools:visibility="visible" />
<com.google.android.gms.common.SignInButton
android:id="@+id/btn_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_login_explain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="구글 로그인이 필요합니다. "
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/btn_login"
app:layout_constraintStart_toStartOf="@id/btn_login"
app:layout_constraintTop_toBottomOf="@+id/btn_login"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
LoginViewModel.kt
internal class LoginViewModel() : ViewModel() {
private var _loginStateLiveData = MutableLiveData<LoginState>(LoginState.UnInitialized)
val loginStateLiveData: LiveData<LoginState> = _loginStateLiveData
fun fetchData(tokenId: String?): Job = viewModelScope.launch {
setState(LoginState.Loading)
tokenId?.let {
setState(
LoginState.Login(it)
)
} ?: kotlin.run {
setState(
LoginState.Success.NotRegistered
)
}
}
/* 로그인 성공 result 받았을 떄 호출 */
fun saveToken(idToken: String) = viewModelScope.launch {
withContext(Dispatchers.IO) {
fetchData(idToken)
}
}
/* 로그인 성공 후 정보 설정 */
fun setUserInfo(firebaseUser: FirebaseUser?) = viewModelScope.launch {
firebaseUser?.let { user ->
setState(
LoginState.Success.Registered(
user.displayName ?: "익명",
user.photoUrl!!,
)
)
} ?: kotlin.run {
setState(LoginState.Success.NotRegistered)
}
}
/* 로그아웃 버튼 클릭 시 호출 */
fun signOut() = viewModelScope.launch {
fetchData(null)
}
private fun setState(state: LoginState) {
_loginStateLiveData.postValue(state)
}
}
LoginState.kt
sealed class LoginState {
object UnInitialized : LoginState()
object Loading : LoginState()
data class Login(
val idToken: String
) : LoginState()
sealed class Success : LoginState() {
data class Registered( //Google Auth 등록된 상태
val userName: String,
val profileImgeUri: Uri,
) : Success()
object NotRegistered : Success() //Google Auth 미등록된 상태
}
object Error : LoginState()
}
결과 화면
감사합니다!
728x90
반응형
'📱 안드로이드 Android ~ Kotlin' 카테고리의 다른 글
[Android/Kotlin] Koin (kotlin 으로 작성된 경량화된 Dependency Injection 프레임워크) (0) | 2021.09.28 |
---|---|
[Android/Kotlin] FCM 푸시 Push 알림 구현하기 (2) | 2021.09.25 |
[Android/Kotlin] 코루틴 coroutine (0) | 2021.09.21 |
[안드로이드/Android] 클린아키텍처 Clean Architecture (0) | 2021.09.09 |
[Android/Firebase] Firebase 프로젝트 생성 및 앱 추가 (0) | 2021.09.05 |