도입
이제 설정 이후 제대로된 구현 글입니다. 앞에 내용인 프로젝트 설정을 못보셨다면 링크를 참고해주세요!
UI에서 카카오톡 구현
사실 앞에서 진행되었던 프로젝트 설정에 비하면 카카오톡 로그인 코드는 굉장히 쉽습니다. (링크)
버튼을 클릭했을 때 이 코드만 넣어주시면 바로 해결됩니다. (만약 설정부분에서 빠진 부분이 있다면 안되겠죠 ㅎㅎ)
// 로그인 조합 예제
// 카카오계정으로 로그인 공통 callback 구성
// 카카오톡으로 로그인 할 수 없어 카카오계정으로 로그인할 경우 사용됨
val callback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
if (error != null) {
Log.e(TAG, "카카오계정으로 로그인 실패", error)
} else if (token != null) {
Log.i(TAG, "카카오계정으로 로그인 성공 ${token.accessToken}")
}
}
// 카카오톡이 설치되어 있으면 카카오톡으로 로그인, 아니면 카카오계정으로 로그인
if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
UserApiClient.instance.loginWithKakaoTalk(context) { token, error ->
if (error != null) {
Log.e(TAG, "카카오톡으로 로그인 실패", error)
// 사용자가 카카오톡 설치 후 디바이스 권한 요청 화면에서 로그인을 취소한 경우,
// 의도적인 로그인 취소로 보고 카카오계정으로 로그인 시도 없이 로그인 취소로 처리 (예: 뒤로 가기)
if (error is ClientError && error.reason == ClientErrorCause.Cancelled) {
return@loginWithKakaoTalk
}
// 카카오톡에 연결된 카카오계정이 없는 경우, 카카오계정으로 로그인 시도
UserApiClient.instance.loginWithKakaoAccount(context, callback = callback)
} else if (token != null) {
Log.i(TAG, "카카오톡으로 로그인 성공 ${token.accessToken}")
}
}
} else {
UserApiClient.instance.loginWithKakaoAccount(context, callback = callback)
}
Kakao Login with MVVM
하지만 제가 이 부분을 포스팅하는 이유는 "카카오톡 로그인 하는 부분도 비즈니스 로직이니까 ViewModel로 빼야하는거 아니야?" 라는 생각때문입니다.
구글에서 "Android Kakao Login MVVM" 이라고 검색했을 때 만족스러운 정리글이 없었습니다. 그래도 정대리님의 카카오톡 로그인 연동 - 유튜브가 제일 만족스러웠지만 따라해보면 한가지 문제점이 발생합니다.
먼저 정대리님의 유튜브를 따라서 ViewModel을 구현해보면 다음과 같습니다.
class ExKakaoLoginViewModel(
private val application: Application
): AndroidViewModel(application) {
private val TAG = ExKakaoLoginViewModel::class.java.simpleName
private val context = application.applicationContext
private val _isLogin = MutableStateFlow<Boolean>(false)
val isLogin = _isLogin.asStateFlow()
fun kakaoLogin() {
viewModelScope.launch {
_isLogin.emit(handleKakaoLogin())
}
}
private suspend fun handleKakaoLogin(): Boolean = suspendCoroutine { continuation ->
// 카카오계정으로 로그인 공통 callback 구성
// 카카오톡으로 로그인 할 수 없어 카카오계정으로 로그인할 경우 사용됨
val callback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
if (error != null) {
Log.e(TAG, "카카오계정으로 로그인 실패", error)
continuation.resume(false)
} else if (token != null) {
Log.i(TAG, "카카오계정으로 로그인 성공 ${token.accessToken}")
continuation.resume(true)
}
}
if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
UserApiClient.instance.loginWithKakaoTalk(context) { token, error ->
if (error != null) {
Log.e(TAG, "카카오톡으로 로그인 실패", error)
// 사용자가 카카오톡 설치 후 디바이스 권한 요청 화면에서 로그인을 취소한 경우,
// 의도적인 로그인 취소로 보고 카카오계정으로 로그인 시도 없이 로그인 취소로 처리 (예: 뒤로 가기)
if (error is ClientError && error.reason == ClientErrorCause.Cancelled) {
return@loginWithKakaoTalk
}
// 카카오톡에 연결된 카카오계정이 없는 경우, 카카오계정으로 로그인 시도
UserApiClient.instance.loginWithKakaoAccount(context, callback = callback)
} else if (token != null) {
Log.i(TAG, "카카오톡으로 로그인 성공 ${token.accessToken}")
}
}
} else {
UserApiClient.instance.loginWithKakaoAccount(context, callback = callback)
}
}
}
2가지 포인트에서 문제가 발생합니다.
1. ViewModel에서의 Context 사용
ViewModel에서는 Context의 사용을 지양하고 있습니다. (링크1, 링크2, StackOverFlow)
가장 근본적인 이유는 Context의 사용이 메모리 누수로 이어지기 때문입니다.
-> Activity는 ViewModel보다 더 많이 생성과 소멸의 과정을 가지게 되는데 이 때 Context를 ViewModel이 참고하고 있을 경우 계속 남아 있어 메모리 누수가 발생하게 됩니다.
Android에서 AndroidViewModel을 통해 Application의 Context를 사용할 수 있도록 제공하곤 있지만 실제로 함수 내부가 아닌 외부 객체로 위의 코드처럼 사용하면 다음과 같은 Warning을 만나게 됩니다.
This field leaks a context object
애플리케이션은 처음 실행부터 완전이 종료될 때까지 살아있기 때문에 메모리 누수가 발생할 가능성은 낮지만 최대한 ViewModel에서 Context에 대한 사용을 줄이는 것이 좋다고 생각합니다.
2. applicationContext로 인한 Calling startActivity() from outside of an Activity 에러
위의 문제만 존재했다면 크게 고려안했을 지도 모르지만 Application의 context로 활용하게 될 경우 다음과 같은 에러를 만나게 됩니다. 즉 이전 글에서 Manifest에 Activity를 추가했던 이유도 만약 카카오톡 로그인이 안될 경우 새로운 Acitivity를 통해 로그인 기능을 처리하는 역할로서 명시한 것인데, Application의 Context로는 이동할 수 없다는 뜻입니다.
android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
이러한 문제들 때문에 UiState를 도입해서 처리하게 되었습니다. (UiState에 대해 알고 싶으시다면 링크를 참고해주세요)
Kakao Login with MVVM + UiState
우선 Login 화면의 상태를 4가지로 정의했습니다.
sealed interface SocialLoginUiState {
object LoginSuccess : SocialLoginUiState
object LoginFail : SocialLoginUiState
object IDle : SocialLoginUiState
object KakaoLogin : SocialLoginUiState
}
ViewModel에서는 StateFlow를 통해 UiState를 관리하게 됩니다.
class SocialLoginViewModel : ViewModel() {
private val _socialLoginUiState = MutableStateFlow<SocialLoginUiState>(SocialLoginUiState.IDle)
val socialLoginUiState = _socialLoginUiState.asStateFlow()
fun kakaoLogin() {
_socialLoginUiState.value = SocialLoginUiState.KakaoLogin
}
fun kakaoLoginSuccess() {
_socialLoginUiState.tryEmit(SocialLoginUiState.LoginSuccess)
}
fun kakaoLoginFail() {
_socialLoginUiState.tryEmit(SocialLoginUiState.LoginFail)
}
fun setUiStateIdle() {
_socialLoginUiState.tryEmit(SocialLoginUiState.IDle)
}
}
대신 Ui에서 Context 필요로 하는 로직을 처리하게 됩니다.
class SocialLoginFragment : Fragment() {
// ...
private val viewModel by viewModels<SocialLoginViewModel>()
// ...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.kakaoLogin.setOnClickListener {
viewModel.kakaoLogin()
}
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.socialLoginUiState.collect { uiState ->
when (uiState) {
SocialLoginUiState.KakaoLogin -> {
handleKakaoLogin()
}
SocialLoginUiState.LoginSuccess -> {
showToast("Login Success")
}
SocialLoginUiState.LoginFail -> {
showToast("Login Fail")
}
else -> {}
}
}
}
}
}
private fun handleKakaoLogin() {
// 카카오계정으로 로그인 공통 callback 구성
// 카카오톡으로 로그인 할 수 없어 카카오계정으로 로그인할 경우 사용됨
val callback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
if (error != null) {
Log.e(TAG, "카카오계정으로 로그인 실패", error)
viewModel.kakaoLoginFail()
} else if (token != null) {
Log.i(TAG, "카카오계정으로 로그인 성공 ${token.accessToken}")
viewModel.kakaoLoginSuccess()
}
}
if (UserApiClient.instance.isKakaoTalkLoginAvailable(requireContext())) {
UserApiClient.instance.loginWithKakaoTalk(requireContext()) { token, error ->
if (error != null) {
Log.e(TAG, "카카오톡으로 로그인 실패", error)
// 사용자가 카카오톡 설치 후 디바이스 권한 요청 화면에서 로그인을 취소한 경우,
// 의도적인 로그인 취소로 보고 카카오계정으로 로그인 시도 없이 로그인 취소로 처리 (예: 뒤로 가기)
if (error is ClientError && error.reason == ClientErrorCause.Cancelled) {
return@loginWithKakaoTalk
}
// 카카오톡에 연결된 카카오계정이 없는 경우, 카카오계정으로 로그인 시도
UserApiClient.instance.loginWithKakaoAccount(requireContext(), callback = callback)
} else if (token != null) {
Log.i(TAG, "카카오톡으로 로그인 성공 ${token.accessToken}")
viewModel.kakaoLoginSuccess()
}
}
} else {
UserApiClient.instance.loginWithKakaoAccount(requireContext(), callback = callback)
}
}
private fun showToast(message: String) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
viewModel.setUiStateIdle()
}
// ...
}
마무리
ViewModel에서 Context를 지양하면서도 Activity Contect를 통해 Kakao Login을 구현하는 방법에 대해 정리해봤습니다.
UiState를 학습한지 얼마 되지 않아서 부족한 코드일 수 있으니 많은 조언 부탁드립니다.
읽어주셔서 감사합니다!
'Android' 카테고리의 다른 글
2024 Android 앱 모듈화 가이드 (Modularization) (1) | 2024.04.25 |
---|---|
GSON과 코틀린을 쓰면서 생겼던 일 (nonnull인데 왜 null 값이 들어갔지?) (0) | 2024.04.16 |
[Android] Kakao Login 구현하기2 - 프로젝트 설정 (0) | 2023.09.30 |
[Android] Kakao Login 구현하기1 - 기본 설정 (1) | 2023.09.30 |
[Android] 도메인 레이어 (0) | 2023.09.30 |
댓글