일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 막내의막무가내 목표 및 회고
- 막내의막무가내 SQL
- 막내의막무가내 플러터
- 프로그래머스 알고리즘
- 막내의막무가내 rxjava
- 막내의막무가내 프로그래밍
- 막내의막무가내 코틀린 안드로이드
- 막내의막무가내 일상
- 막내의막무가내 코틀린
- 주택가 잠실새내
- 2022년 6월 일상
- flutter network call
- 막내의막무가내 안드로이드 코틀린
- 막내의막무가내 안드로이드 에러 해결
- 프래그먼트
- 부스트코스에이스
- 안드로이드
- 막내의 막무가내 알고리즘
- 막내의막무가내
- 막무가내
- 주엽역 생활맥주
- 막내의막무가내 알고리즘
- Fragment
- 막내의막무가내 코볼 COBOL
- 막내의막무가내 안드로이드
- 부스트코스
- 안드로이드 sunflower
- 막내의 막무가내
- 막내의막무가내 플러터 flutter
- 안드로이드 Sunflower 스터디
- Today
- Total
막내의 막무가내 프로그래밍 & 일상
[안드로이드] 안드로이드 카카오톡 로그인 구현 및 MVVM 적용 (feat. SingleLiveEvent) 본문
[안드로이드] 안드로이드 카카오톡 로그인 구현 및 MVVM 적용 (feat. SingleLiveEvent)
막무가내막내 2020. 6. 22. 17:06[2021-04-13 업데이트]
카카오 로그인 API가 업데이트되어 다른 내용이 있을 수 있습니다. 공식문서가 잘 되어 있으므로 공식문서 위주로 참고하시고 모르는게 있으면 이 포스팅에서 찾아보심이 좋을 것 같습니다. 감사합니다 :)
https://youngest-programming.tistory.com/93
이전에 위와 같이 카카오톡 로그인을 구현한적 있었는데 1년 사이에 v1 -> v2 로 바뀌면서 구현방식이 달라졌나봅니다.
그 새로운 버전에 대해서는 공식문서와
블로그로는 밑을 참고했습니다.
참고 : https://lakue.tistory.com/40
처음에 MVC 방식으로 구현한 후
MVVM 으로 카카오톡 로그인을 로직을 변경해봤습니다.
다음과 같이 구현했습니다.
먼저 카카오톡 로그인 관련 디펜던시를 추가해줍니다.
이런 기본설정이나 카카오톡로그인시에 필요한 클래스들은 위 블로그 사이트들을 들어가거나 공식문서에 가면 자세하게 나옵니다. (그래서 몇개는 생략한게 있을 수 있습니다.)
[app gradle]
/*카카오로그인*/
implementation group: project.KAKAO_SDK_GROUP, name: 'usermgmt', version: project.KAKAO_SDK_VERSION
[project gradle]
allprojects {
repositories {
google()
jcenter()
mavenCentral()
maven{
url "https://maven.google.com"
}
/*카카오*/
maven { url 'http://devrepo.kakao.com:8088/nexus/content/groups/public/'}
}
}
[gradle.properties]
#kakao login
KAKAO_SDK_GROUP=com.kakao.sdk
KAKAO_SDK_VERSION=1.27.0
카카오 API 키와 Hash Key를 각각 안드로이드와 카카오개발자관리(?) 사이트에 필요하고 등록해야한다. (이것은 위에 참조된 블로그들에 가면 자세히 나오니 참고 바랍니다.)
LoginActivity 라는 곳을 중심으로 구현했습니다.
[manifest]
Application(name) 을 참조하고 meta-data 넣었음을 참고!
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mtjin.nomoneytrip">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".di.KoinApplication"
android:allowBackup="true"
android:icon="@drawable/logo"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<meta-data
android:name="com.kakao.sdk.AppKey"
android:value="@string/kakao_app_key" />
<activity android:name=".views.login.LoginActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
[KoinApplication]
저는 Koin을 사용해서 카카오톡 관련 코드와 Koin 관련 코드가 섞여 있습니다. startKoin{} 빼고 카카오톡 로그인 관련 코드입니다.
package com.mtjin.nomoneytrip.di
import android.app.Application
import com.kakao.auth.*
import com.mtjin.nomoneytrip.BuildConfig
import com.mtjin.nomoneytrip.module.*
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import org.koin.core.logger.Level
class KoinApplication : Application() {
override fun onCreate() {
super.onCreate()
instance = this
// Kakao Sdk 초기화
KakaoSDK.init(KakaoSDKAdapter())
startKoin {
if (BuildConfig.DEBUG) {
androidLogger()
} else {
androidLogger(Level.NONE)
}
androidContext(this@KoinApplication)
modules(
repositoryModule,
localDataModule,
remoteDataModule,
viewModelModule,
apiModule
)
}
}
override fun onTerminate() {
super.onTerminate()
instance = null
}
class KakaoSDKAdapter : KakaoAdapter() {
override fun getSessionConfig(): ISessionConfig {
return object : ISessionConfig {
override fun getAuthTypes(): Array<AuthType> {
return arrayOf(AuthType.KAKAO_LOGIN_ALL)
}
override fun isUsingWebviewTimer(): Boolean {
return false
}
override fun isSecureMode(): Boolean {
return false
}
override fun getApprovalType(): ApprovalType? {
return ApprovalType.INDIVIDUAL
}
override fun isSaveFormData(): Boolean {
return true
}
}
}
// Application이 가지고 있는 정보를 얻기 위한 인터페이스
override fun getApplicationConfig(): IApplicationConfig {
return IApplicationConfig { getGlobalApplicationContext() }
}
}
companion object {
private var instance: KoinApplication? = null
fun getGlobalApplicationContext(): KoinApplication? {
checkNotNull(instance) { "This Application does not inherit com.kakao.GlobalApplication" }
return instance
}
}
}
뺴먹었는데 프로젝트 구조는 다음과 같습니다.
카카오톡 로그인에 필요한 세션콜백(SessionCallback) 클래스입니다. 뷰에서 가져다가 사용할 것 입니다.
package com.mtjin.nomoneytrip.views.login
import android.util.Log
import com.kakao.auth.ISessionCallback
import com.kakao.network.ErrorResult
import com.kakao.usermgmt.UserManagement
import com.kakao.usermgmt.callback.MeV2ResponseCallback
import com.kakao.usermgmt.response.MeV2Response
import com.kakao.util.exception.KakaoException
class SessionCallback : ISessionCallback {
// 로그인에 성공한 상태
override fun onSessionOpened() {
requestMe()
}
// 로그인에 실패한 상태
override fun onSessionOpenFailed(exception: KakaoException) {
Log.e(LoginActivity.TAG, "onSessionOpenFailed : " + exception.message)
}
// 사용자 정보 요청
private fun requestMe() {
UserManagement.getInstance()
.me(object : MeV2ResponseCallback() {
override fun onSessionClosed(errorResult: ErrorResult) {
Log.e(LoginActivity.TAG, "세션이 닫혀 있음: $errorResult")
}
override fun onFailure(errorResult: ErrorResult) {
Log.e(LoginActivity.TAG, "사용자 정보 요청 실패: $errorResult")
}
override fun onSuccess(result: MeV2Response) {
Log.i(LoginActivity.TAG, "사용자 아이디: " + result.id)
}
})
}
}
다음은 액티비티 관련 클래스 입니다.
[BaseActivity]
package com.mtjin.nomoneytrip.base
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import io.reactivex.disposables.CompositeDisposable
abstract class BaseActivity<B : ViewDataBinding>(
@LayoutRes val layoutId: Int
) : AppCompatActivity() {
lateinit var binding: B
private val compositeDisposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, layoutId)
binding.lifecycleOwner = this
}
protected fun showToast(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
}
}
[LoginActivity]
위 BaseActivity를 상속받습니다. 데이터바인딩 작업과 뷰모델의 카카오톡 로그인을 옵저빙하는 코드와 세션 초기화하는 코드가 있습니다.
package com.mtjin.nomoneytrip.views.login
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.Observer
import com.kakao.auth.AuthType
import com.kakao.auth.ISessionCallback
import com.kakao.auth.Session
import com.kakao.util.exception.KakaoException
import com.mtjin.nomoneytrip.R
import com.mtjin.nomoneytrip.base.BaseActivity
import com.mtjin.nomoneytrip.databinding.ActivityLoginBinding
import com.mtjin.nomoneytrip.views.main.MainActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
class LoginActivity : BaseActivity<ActivityLoginBinding>(R.layout.activity_login) {
private val viewModel: LoginViewModel by viewModel()
// 세션 콜백 구현
private val sessionCallback: ISessionCallback = object : ISessionCallback {
override fun onSessionOpened() {
Log.i(TAG, "로그인 성공")
val intent: Intent = Intent(this@LoginActivity, MainActivity::class.java)
startActivity(intent)
finish()
}
override fun onSessionOpenFailed(exception: KakaoException) {
Log.e(TAG, "로그인 실패", exception)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.vm = viewModel
Session.getCurrentSession().addCallback(sessionCallback)
initViewModelCallback()
}
private fun initViewModelCallback() {
with(viewModel) {
kakaoLogin.observe(this@LoginActivity, Observer {
kakaoLogin.value?.addCallback(SessionCallback())
kakaoLogin.value?.open(AuthType.KAKAO_LOGIN_ALL, this@LoginActivity)
})
}
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
// 카카오톡|스토리 간편로그인 실행 결과를 받아서 SDK로 전달
if (Session.getCurrentSession().handleActivityResult(requestCode, resultCode, data)) {
return
}
super.onActivityResult(requestCode, resultCode, data)
}
override fun onDestroy() {
super.onDestroy()
// 세션 콜백 삭제
Session.getCurrentSession().removeCallback(sessionCallback)
}
companion object {
const val TAG: String = "LoginActivityTAG"
}
}
utils에 선언해 놓은 클래스인데 MutableLiveData 를 상속받아 구현된 클래스 입니다. LiveData 가 불필요하게 옵저빙 되는 것을 방지해주는데
자세한 내용은 다음을 참고합니다.
https://www.charlezz.com/?p=1152
[SingleLiveEvent] 이것을 ViewModel에서 MutableLiveData 대신 사용할 것입니다
package com.mtjin.nomoneytrip.utils
import android.util.Log
import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean
open class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
@MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {
value = null
}
companion object {
private val TAG = "SingleLiveEvent"
}
}
다음은 뷰모델 클래스입니다.
[BaseViewModel]
package com.mtjin.nomoneytrip.base
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import io.reactivex.disposables.CompositeDisposable
abstract class BaseViewModel : ViewModel() {
protected val compositeDisposable = CompositeDisposable()
private val _isLoading = MutableLiveData<Boolean>(false)
val isLoading: LiveData<Boolean> get() = _isLoading
override fun onCleared() {
compositeDisposable.dispose()
super.onCleared()
}
protected fun showProgress() {
_isLoading.value = true
}
protected fun hideProgress() {
_isLoading.value = false
}
}
[ LoginViewModel ]
앞서 정의한 SingleLiveEvent 를 사용하여 로그인시마다 액티비티에 콜백을 보내주도록 했습니다. 그리고 카카오톡 로그인 버튼 클릭시 kakaoLogin() 이 호출됩니다. (xml에서 onClick 정의해놨습니다.)
package com.mtjin.nomoneytrip.views.login
import androidx.lifecycle.LiveData
import com.kakao.auth.Session
import com.mtjin.nomoneytrip.base.BaseViewModel
import com.mtjin.nomoneytrip.data.login.source.LoginRepository
import com.mtjin.nomoneytrip.utils.SingleLiveEvent
class LoginViewModel(private val loginRepository: LoginRepository) : BaseViewModel() {
private val _kakaoLogin = SingleLiveEvent<Session>()
val kakaoLogin: LiveData<Session> get() = _kakaoLogin
fun kakaoLogin() {
_kakaoLogin.value = loginRepository.kakaoLogin()
}
}
Model 에 들어가기전 XML 코드 입니다.
뷰모델을 가져다 쓰고 카카오톡 로그인시 뷰모델 함수가 호출됩니다. (참고로 카카오톡 로그인 버튼은 기존 카카오톡에서 제공하는것이아닌 커스텀으로 구현했습니다.)
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="vm"
type="com.mtjin.nomoneytrip.views.login.LoginViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".views.login.LoginActivity">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_app_logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="160dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_app_main_logo" />
<com.skydoves.elasticviews.ElasticImageView
android:id="@+id/iv_kakao_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="131dp"
android:onClick="@{()->vm.kakaoLogin()}"
app:imageView_duration="300"
app:imageView_scale="0.7"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_app_logo"
app:srcCompat="@drawable/button_kakao" />
<com.skydoves.elasticviews.ElasticImageView
android:id="@+id/iv_email_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:imageView_duration="300"
app:imageView_scale="0.7"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_kakao_login"
app:srcCompat="@drawable/button_email" />
<com.skydoves.elasticviews.ElasticImageView
android:id="@+id/iv_signup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="21dp"
app:imageView_duration="300"
app:imageView_scale="0.7"
app:layout_constraintEnd_toStartOf="@id/view"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_email_login"
app:srcCompat="@drawable/button_signup" />
<View
android:id="@+id/view"
android:layout_width="1dp"
android:layout_height="12dp"
android:background="@color/colorWhiteGray"
app:imageView_duration="300"
app:imageView_scale="0.7"
app:layout_constraintBottom_toBottomOf="@id/iv_signup"
app:layout_constraintEnd_toStartOf="@id/iv_question"
app:layout_constraintStart_toEndOf="@id/iv_signup"
app:layout_constraintTop_toTopOf="@id/iv_signup" />
<com.skydoves.elasticviews.ElasticImageView
android:id="@+id/iv_question"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="21dp"
app:imageView_duration="300"
app:imageView_scale="0.7"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/view"
app:layout_constraintTop_toBottomOf="@id/iv_email_login"
app:srcCompat="@drawable/button_question" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
참고 : model 쪽은 Local은 아직 구현안했고 (자동로그인 구현예정) 로그인 기본적인 기능만 구현한 것이기 때문에 양이 적습니다.
레포지토리입니다.
[LoginRepositoryImpl]
package com.mtjin.nomoneytrip.data.login.source
import com.kakao.auth.Session
import com.mtjin.nomoneytrip.data.login.source.remote.LoginRemoteDataSource
class LoginRepositoryImpl(private val loginRemoteDataSource: LoginRemoteDataSource) :
LoginRepository {
override fun kakaoLogin(): Session = loginRemoteDataSource.kakaoLogin()
}
RemoteDataSource 입니다.
[LoginRemoteDataSourceImpl]
package com.mtjin.nomoneytrip.data.login.source.remote
import com.kakao.auth.Session
class LoginRemoteDataSourceImpl : LoginRemoteDataSource {
override fun kakaoLogin(): Session = Session.getCurrentSession()
}
LocalDataSource 는 만들어만 놨고 구현된건 아직 없습니다.
최종 화면 입니다.
아직 미완성이고 부족하지만 카카오톡 로그인 관련을 기존 MVC 에서 MVVM으로 변경해보았습니다.
댓글과 공감은 큰 힘이 됩니다. 감사합니다!!
'안드로이드 > 코틀린 & 아키텍처 & Recent' 카테고리의 다른 글
[안드로이드] Jetpack Navigation Fragment to Fragment direction (젯팩 네비게이션 프래그먼트 화면 전환) + 값 주고받는 방법 (0) | 2020.06.26 |
---|---|
[안드로이드] 안드로이드 BottomNavigationView icon 설정 해결 (8) | 2020.06.26 |
[안드로이드] ViewModel 에서 context 필요로 할 때 해결방법 (0) | 2020.06.22 |
[안드로이드] 안드로이드 MVVM + RxJava2 + Koin + Jsoup 정리 (충남대학교 컴퓨터공학과 공지사항 토이 프로젝트) (4) | 2020.06.03 |
[안드로이드] 코틀린 커스텀 다이얼로그 프래그먼트 (Custom Dialog Fragment) (3) | 2020.05.23 |