0️⃣ 리팩토링하는 글이다.
위 게시글에서 이어지는 내용이다. 기본 세팅은 모두 생략하고 코드를 어떻게 리팩토링 하는지 차근차근 작성할 것이다.
우선, 내가 따른 구조는 튜터님께서 손수 짜주신 구조로 팀원들과 함께 응용했다. 근데 너무 어렵다. 아직도 익숙해지지가 않은 것 같지만, 이번 리팩토링을 통해 조금 감을 익혀가는 중이므로 기록을 통해 세세하게 적어보겠다.
1️⃣ 기존 SignInActivity
class SignInActivity : AppCompatActivity() {
private lateinit var auth: FirebaseAuth
val firestoreDB = FirebaseFirestore.getInstance()
private lateinit var googleSignInClient: GoogleSignInClient
private val signInBinding by lazy { ActivitySignInBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(signInBinding.root)
auth = FirebaseAuth.getInstance()
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build()
googleSignInClient = GoogleSignIn.getClient(this, gso)
signInBinding.ivGoogle.setOnClickListener {
googleLogin()
}
}
private fun googleLogin() {
val signInIntent = googleSignInClient.signInIntent
googleLauncher.launch(signInIntent)
}
private val googleLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
try {
val account = task.getResult(ApiException::class.java)!!
firebaseAuthWithGoogle(account.idToken!!)
} catch (e: ApiException) {
Toast.makeText(this, "Google 로그인에 실패했습니다.", Toast.LENGTH_SHORT).show()
}
}
private fun firebaseAuthWithGoogle(idToken: String) {
val credential = GoogleAuthProvider.getCredential(idToken, null)
auth.signInWithCredential(credential)
.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
Toast.makeText(this, "환영합니다, ${user?.displayName}!", Toast.LENGTH_SHORT).show()
startActivity(Intent(this, MainActivity::class.java))
finish()
} else {
Toast.makeText(this, "Firebase 인증에 실패했습니다.", Toast.LENGTH_SHORT).show()
}
}
}
}
우선 이 코드는 저번 게시물에서 정상적으로 잘 작동하는 구글 로그인 코드다. 가장 먼저 해야할 일은 우선 View에서 선언된 함수를 모두 ViewModel로 분리해주어야 한다.
💡 그 전에 참고할 사항!
❗️ 기본적으로 Hilt를 함께 사용하여 의존성 주입을 보다 용이하게 할 수 있게 하였다.
❗️ ViewModel에서 View를 조작할 수 없었기 때문에 UiState를 추가 하였다.
2️⃣ SignInViewModel
@HiltViewModel
class SignInViewModel @Inject constructor(
private val repository: SignInRepository,
) : ViewModel() {
private val _user = MutableStateFlow<FirebaseUser?>(null)
private var auth: FirebaseAuth = FirebaseAuth.getInstance()
private val _status = MutableStateFlow<UiState<FirebaseUser>>(UiState.Init)
val status: StateFlow<UiState<FirebaseUser>> = _status.asStateFlow()
fun googleLogin(googleSignInClient: GoogleSignInClient, launcher: ActivityResultLauncher<Intent>) {
val signInIntent = googleSignInClient.signInIntent
launcher.launch(signInIntent)
}
fun googleLauncherFunction(result: ActivityResult) {
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
try {
val account = task.getResult(ApiException::class.java)!!
firebaseAuthWithGoogle(account.idToken!!)
} catch (e: ApiException) {
_status.value = UiState.Failure(e)
Log.e("LOGIN-- FAILURE: googleLauncherFunction", e.toString())
}
}
private fun firebaseAuthWithGoogle(idToken: String) {
val credential = GoogleAuthProvider.getCredential(idToken, null)
auth.signInWithCredential(credential)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
_user.value = auth.currentUser
repository.setDataToFireBase()
_status.value = UiState.Success(_user.value!!)
} else {
_status.value = UiState.Failure(task.exception)
Log.e("LOGIN-- FAILURE: firebaseAuthWithGoogle", "Firebase 인증에 실패했습니다.")
}
}
}
}
- 가장 먼저 googleLogin() 함수를 ViewModel로 가져왔다.
- ViewModel에서 선언되어 사용할 수 없는 값들은 모두 Activity에서 받아올 수 있도록 매개변수로 지정해주었다.
- googleLauncherFunction() 함수는 사실 registerForActivityResult 내에서 실행될 함수이다. registerForActivityResult는 ViewModel에서 따로 조작할 수 없기 때문에 직접적인 실행은 Activity에서 해주고 내부 코드만 따로 분리한 것이다. 때문에 result값도 매개변수로 받아 이용했다.
- firebaseAuthWithGoogle() 함수는 어렵지 않게 가져올 수 있었으나 실질적으로 Firebase와 소통하는 코드는 Repository로 분리했다.
- status 라는 이름의 UiState를 추가하여 로그인이 성공했을 때와 실패했을 때를 구분하여 View에서도 이를 감지할 수 있도록 하였다.
- 로그인이 성공하면 UiState.Success를, 실패하면 Failure로 구분한다.
3️⃣ SignInRepository & SignInRepositoryImpl
interface SignInRepository {
fun setDataToFireBase()
}
class SignInRepositoryImpl @Inject constructor(
private val firestoreDB: FirebaseFirestore,
private val firebaseAuth: FirebaseAuth,
) : SignInRepository {
override fun setDataToFireBase() {
val authUser = firebaseAuth.currentUser
val uId = authUser?.uid.toString()
val userRef = firestoreDB.collection("users").document(uId)
userRef.get().addOnSuccessListener { document ->
if (document.exists()) {
val userData = document.data
// 재로그인
// TODO : 해당 코드 정상작동하는지 검토 필요 (데이터 직접 넣어보며 테스트 요망)
if (userData != null) {
userRef.set(userData)
.addOnSuccessListener { Log.d("User Data 업데이트 성공", "User data is successfully updated!!") }
.addOnFailureListener { exception -> Log.w("User Data 업데이트 실패", "Error updating document", exception) }
}
} else {
// 최초 로그인 (회원가입)
val firstUser = hashMapOf(
"uID" to uId,
"name" to authUser?.displayName.toString(),
...
)
userRef.set(firstUser)
.addOnSuccessListener { Log.d("User Data 전송 성공", "User data is successfully written!") }
.addOnFailureListener { exception -> Log.w("User Data 전송 실패", "Error writing document", exception) }
}
}
}
}
- Firestore와 소통하는 코드를 작성하기 위해 Repository에서 interface를 생성하고 이를 RepositoryImpl에서 작성한다.
- fireStoreDB의 collection에 uId 값으로 접근하고 만약 해당 uId의 document가 있다면 재로그인이므로 userData를 업데이트 해준다.
- 반대로 uId 값의 document가 없으면 최초 로그인, 즉 회원가입이므로 새로운 값을 넣어주어 userData를 추가한다.
4️⃣ SignInActivity
@AndroidEntryPoint
class SignInActivity : AppCompatActivity() {
private val signInBinding by lazy { ActivitySignInBinding.inflate(layoutInflater) }
private val signInViewModel by viewModels<SignInViewModel>()
val googleSignInClient: GoogleSignInClient by lazy {
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) //기본 로그인 방식 사용
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build()
GoogleSignIn.getClient(this, gso)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(signInBinding.root)
signInBinding.ivGoogle.setOnClickListener {
signInViewModel.googleLogin(googleSignInClient, launcher)
}
}
private val launcher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
signInViewModel.googleLauncherFunction(result)
signInObserve(this)
}
private fun signInObserve(context: Context) {
lifecycleScope.launch {
signInViewModel.status.collectLatest {
when (it) {
is UiState.Success -> {
val intent = Intent(context, MainActivity::class.java)
startActivity(intent)
}
else -> {
// 로그인 실패 또는 로딩 중
// TODO : Dialog 넣기
}
}
}
}
}
}
- 굳이 ViewModel로 분리해줄 필요가 없거나, 할 수 없는 로직들은 (View 조작) Activity에서 작성해주면 된다.
- 특별한 점은 observer를 추가해서 UiState값에 따라 로그인이 성공하면 MainActivity로 이동하고 아니라면 로그인 실패 Dialog를 띄워주는 것 뿐이다.
5️⃣ 아 맞다 UseCase
실수로 UseCase로 분리하는 것을 깜빡했다. 우선적으로 코드가 큰 에러 없이 작동하고 있으며 자잘한 동작오류나 섬세한 데이터 베이스 연동을 위해 UseCase로 분리하는 것은 MVP 기능개발 이후에 하도록 하겠다! 그 때 마저 포스팅 해야지...
'Android' 카테고리의 다른 글
Android와 선언형 UI : Jetpack Compose (5) | 2024.10.15 |
---|---|
Android에서 Firebase Google Login 구현하기 (0) | 2024.08.31 |
Hilt로 Context 사용하기 (feat. OkHttpClient cache) (0) | 2024.08.29 |
Android에서 CustomDialog 만들기 (0) | 2024.08.27 |
Android의 4대 구성요소 알아보기 (0) | 2024.08.14 |