https://velog.io/@evergreen_tree/Android-MVVM-AAC-FireBase-Google-Logind 에서 이어집니다!
다음 예제는 FireBase를 통해 Google 로그인을 구현하는 예제입니다.
AAC와 MVVM을 이용해 구현해 보았습니다.
간단하게 두 패키지로 구분하였습니다.
repository에는 Repository, ui에는 Activity와 ViewModel이 있습니다.
class AuthRepository(
) {
private val firebaseAuth = FirebaseAuth.getInstance()
private val _userLiveData = MutableLiveData<FirebaseUser>()
val userLiveData: LiveData<FirebaseUser>
get() = _userLiveData
fun getUser(idToken: String) {
val credential = GoogleAuthProvider.getCredential(idToken, null)
firebaseAuth.signInWithCredential(credential).addOnCompleteListener {
if (it.isSuccessful) {
_userLiveData.postValue(firebaseAuth.currentUser)
} else {
//실패처리
}
}
}
}
먼저 Repository입니다. MVVM의 Model 부분을 담당하게 됩니다.
ViewModel에서 위 코드를 작성하고 처리하지 않는 이유는, 역시나 유지보수 때문입니다.
위 예제는 단순히 Firebase 로그인만을 구현하고 있지만, DB에서 다른 테이블을 조회할 수도 있고, Github API를 통해 어떤 데이터를 가져올 수도 있습니다. 그러한 단위들을 Repository로 구현하여 제공한다면 좀 더 효율적으로 코드를 관리할 수 있습니다.
AAC에서 제공하는 라이브러리로, RxJava,와 유사하게 Observer 패턴을 통해 데이터를 구독하고 발행할 수 있습니다. (추후 Observer 패턴에 대해 포스팅 하겠습니다.)
Repository에서 LiveData 형태로 ViewModel에 제공함으로써, 추후 Activity가 데이터 변경을 관찰하여 View에 보여줄 수 있게 할 것입니다.
class MainViewModel : ViewModel(){
private var authRepository: AuthRepository = AuthRepository()
private val _userLiveData = authRepository.userLiveData
val userLiveData: LiveData<FirebaseUser>
get() = _userLiveData
fun getUser(idToken: String){
authRepository.getUser(idToken)
}
}
View는 ViewModel에 대해 알고 있기 때문에, getUser을 호출하여 데이터를 변경시킬 수 있습니다.
즉, View의 Event를 통해 ViewModel의 메소드를 실행시켜 주면, Repository를 알고있는 ViewModel은 데이터 변경을 요청한다는 의미입니다.
fun getUser(idToken: String) {
...
firebaseAuth.signInWithCredential(credential).addOnCompleteListener {
if (it.isSuccessful) {
_userLiveData.postValue(firebaseAuth.currentUser)
}
...
}
}
ViewModel에서 데이터 변경을 요청하였고, 성공시 liveData를 currentUser로 변경합니다.
Activity는 이 LiveData를 ViewModel을 통해 관찰하고 있기 때문에, 이 변경을 감지하게 됩니다.
class MainActivity : AppCompatActivity() {
private lateinit var getResult: ActivityResultLauncher<Intent>
private val viewModel: MainViewModel by viewModels()
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
private lateinit var googleSignInClient: GoogleSignInClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setGoogleLogin()
viewModel.userLiveData.observe(this) {
binding.textView.text = it.displayName
}
binding.loginButton.setOnClickListener {
login()
}
getResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
try {
val account = task.getResult(ApiException::class.java)!!
viewModel.getUser(account.idToken!!)
Toast.makeText(this, "로그인 성공", Toast.LENGTH_SHORT)
.show()
} catch (e: ApiException) {
Toast.makeText(this, e.localizedMessage, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun login() {
getResult.launch(googleSignInClient.signInIntent)
}
private fun setGoogleLogin() {
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(BuildConfig.WEB_CLIENT_ID)
.requestEmail()
.build()
googleSignInClient = GoogleSignIn.getClient(this, gso)
}
}
액티비티 코드입니다. 중요한 부분만 짚고 넘어가겠습니다.
private val viewModel: MainViewModel by viewModels()
여러가지 방법이 있을 수 있겠지만, Android KTX
라이브러리를 통해 뷰모델을 위임하였습니다.
여기서 뷰모델을 생성하게 되는데, 여기서 중요한 부분이 있습니다.
ViewModel은 activity, Fragment 등의 Context를 참조하는 클래스를 가지고 있으면 안됩니다.
https://velog.io/@evergreen_tree/안드로이드-액티비티-생명주기 먼저 보고 가시면 좋습니다.
ViewModel의 생명주기는 View의 생명주기와 다릅니다. 기기의 화면이 회전되어 액티비티가 onDestroy를 호출할 때에도 ViewModel은 살아있습니다. 잘못되면 Memory Leak(필요하지 않은 메모리를 계속 점유하고 있는 현상)이 발생할 수 있습니다.
따라서 intent 같은 activity를 참조하는 메서드도 ViewModel에서 실행하지 않았습니다.
하지만 여기서 또 문제가 생깁니다. MVVM을 지키려면 intent 결과 메서드도 ViewModel에 작성해야 하는 거 아니야?
override fun onCreate(savedInstanceState: Bundle?) {
...
binding.loginButton.setOnClickListener {
login()
}
getResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
try {
val account = task.getResult(ApiException::class.java)!!
viewModel.getUser(account.idToken!!)
Toast.makeText(this, "로그인 성공", Toast.LENGTH_SHORT)
.show()
} catch (e: ApiException) {
Toast.makeText(this, e.localizedMessage, Toast.LENGTH_SHORT).show()
}
}
}
}
...
private fun login() {
getResult.launch(googleSignInClient.signInIntent)
}
필자도 저 위의 짧은 코드를 작성하기 위해 많은 고민을 했습니다.
Google 로그인은 intent를 통해 유저의 토큰을 가져오게 되는데, 이는 분명히 UI와 관련되지 않은 로직일 것이고 ViewModel에 들어가야 한다고 생각했습니다.
그래서 ViewModel을 생성할 때, interface를 만들어 제공하고 Callback형식으로 사용하는 것은 어떤가 고민을 하였지만,, 실력 좋은 현업자의 이야기도 들어보고, 여러 사람들의 코드를 확인해본 결과 intent 로직은 Activity에서 처리하는 것이 좋다고 생각하였습니다.
디자인 패턴에 너무 얽매이는 것은 좋지 않다. 디자인패턴, 아키텍처는 결국 협업을 위해 존재하는 것이다. 물론 디자인 패턴을 지키지 말라는 것이 아니다. 타당한 이유가 있다면, 어느 정도 틀 안에서 같이 일하는 동료들과의 협업을 위해 어떠한 룰을 만들어 정하면 되는 것이다.
Data Obersving
override fun onCreate(savedInstanceState: Bundle?) {
...
viewModel.userLiveData.observe(this) {
binding.textView.text = it.displayName
}
...
위 코드가 Activity에서 뷰모델의 데이터를 관찰하고 있는 부분입니다.
button onclick → intent → viewModel.getUser() → repository.getUser() 을 통해 데이터가 변경되면,
이렇게 관찰하고 있다가 바뀐 데이터를 View에 보여주게 됩니다.
긴 글 읽어주셔서 감사합니다.
github : https://github.com/EvergreenTree97/MVVM-LiveData-Firebase-Example
Android KTX라..아주 빠르겠군요 ...