📱 안드로이드 Android ~ Kotlin

[Android/Kotlin] 구글 Google 계정 로그인 연동을 위한 firebase console 설정방법 및 소스코드

핑크빛연어 2021. 9. 22. 09:46

 

안녕하세용! 안드로이드 앱에서 Firebase 를 활용하여 Google 로그인 하는 방법 및 소스코드 입니다.

 

 

1. Firebase Auth Google 설정 방법

 

https://eunoia3jy.tistory.com/129

 

[Android/Firebase] Firebase 프로젝트 생성 및 앱 추가

안녕하세용! 안드로이드 앱에서 Firebase 를 활용하기 위해 프로젝트를 생성하고 앱을 추가 하는 방법입니다. 먼저 Firebase 콘솔창으로 이동하여 로그인 해주세요~ https://console.firebase.google.com/?hl=ko..

eunoia3jy.tistory.com

 

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
반응형