‼️ 사전 작업 retrofit ,coroutine2, lifecycle dependency 추가
build.gradle(module.app)
//retrofit2
val retrofit_version="2.9.0"
implementation ("com.squareup.retrofit2:retrofit:$retrofit_version")
implementation ("com.squareup.retrofit2:converter-gson:$retrofit_version")
//okhttp3
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.12.0"))
implementation("com.squareup.okhttp3:okhttp")
implementation("com.squareup.okhttp3:logging-interceptor")
// Gson
implementation ("com.google.code.gson:gson:2.10.1")
//coroutine
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0-RC2")
// ViewModel, lifecycle
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
implementation("androidx.collection:collection-ktx:1.3.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
LoginRequest.kt
client에서 서버로 api요청시의 데이터들
import com.google.gson.annotations.SerializedName
data class LoginRequest(
@SerializedName("email")
var email:String,
@SerializedName("password")
var password:String
)
LoginResponse.kt
api post 요청에 대한 응답 데이터들
BaseResponse.kt
loginResponse를 바로 처리하는게 아니라 베이스 코드를 생성해 두고 거기에서 Loading, Success,Error 로 나누어 한단계 거친 다음에 response 수신
T,out등의 개념은→generics
참고자료
Kotlin - Generics 클래스, 함수를 정의하는 방법
제네릭 타입을 사용하면 한 번의 구현으로 다양한 타입을 사용할 수 있다. 아래 코드에서는 Success안 data에 무슨 타입이 들어오든 괜찮다!
sealed class BaseResponse<out T> {
data class Success<out T>(val data: T? = null) : BaseResponse<T>()
data class Loading(val nothing: Nothing?=null) : BaseResponse<Nothing>()
data class Error(val msg: String?) : BaseResponse<Nothing>()
}
ApiClient.kt -객체 생성
object ApiClient{
var mHttpLoggingInterceptor=HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY) //1.
var mOkHttpClient=OkHttpClient
.Builder()
.addInterceptor(mHttpLoggingInterceptor) //
.build()
var mRetrofit:Retrofit?=null
val client:Retrofit?
get(){
if(mRetrofit==null){
mRetrofit=Retrofit.Builder()
.baseUrl(BASEURL)
.client(mOkHttpClient) //2.
.addConverterFactory(GsonConverterFactory.create())//gson:google에서 만든 java용 json
.build()
}
return mRetrofit
}
}
okHttpClient와 retrofit2를 동시에 이용함→각각의 장점이 존재 (retrofit2가 okHttpClient를 기반으로 만들어졌지만 동시이용)
ApiClient 생성 과정
- LogginInterceptor생성(통신과정 로그 출력시 유용)
- LogginInterceptor를 얹어서 OkHttpClient 생성
- OkHttpClient를 기반으로 retrofit client생성
UserApi.kt
이 친구가 이제 서버에 보낼 api interface
interface UserApi {
@POST("/login")
suspend fun loginUser(
@Body loginRequest:LoginRequest)
: Response<LoginResponse>
companion object{
fun getApi():UserApi?{
return ApiClient.client?.create(UserApi::class.java)
}
}
}
UserRepository.kt
class UserRepository {
suspend fun loginUser(loginRequest: LoginRequest):
Response<LoginResponse>?{
return UserApi.getApi()?.loginUser(loginRequest=loginRequest)
}
//UserApi 인터페이스의 loginUser함수 실행
}
LoginViewModel.kt
loginResult를 MutableLiveData로 지정하여 data 변경사항을 알려줌
val userRepo=UserRepository()//viewmodel 생성자?
val loginResult: MutableLiveData<BaseResponse<LoginResponse>> = MutableLiveData()
loginResult의 초기 값은 BaseResponse의 loading으로 설정
fun loginUser(email:String,pwd:String){
loginResult.value=BaseResponse.Loading()
//viewModelScope.launch를 이용하여 코루틴 실행!
viewModelScope.launch {
try{
val loginRequest=LoginRequest(
email=email,
password=pwd
)//email과 pwd를 담아서 LoginRequest를 생성
val response=userRepo.loginUser(loginRequest=loginRequest)
//위에서 생성한 loginRequest로 usereRepo.loginUser()실행->서버로 로그인 요청
if(response?.code()==200){
loginResult.value=BaseResponse.Success(response.body())
//성공시: BaseResponse->Success
}
else{
loginResult.value=BaseResponse.Error(response?.message())
//실패시: BaseResponse->Error
}
}catch(ex:Exception){
loginResult.value=BaseResponse.Error(ex.message)
}
}
}
TokenManager.kt
서버에게 login api 호출에 대한 응답으로 받은 token을 관리하는 코드
특별할건없다. 그저 “USER_INFO”라는 이름의 sharedPreference에 “USER_TOKEN”으로 token저장 및 가져오기~ 끝!
object TokenManager {
const val USER_TOKEN="user_token"
const val USER_INFO="user_info"
fun saveToken(context:Context,token:String){
val prefs: SharedPreferences =
context.getSharedPreferences(USER_INFO, Context.MODE_PRIVATE)
val editor = prefs.edit()
editor.putString(USER_TOKEN, token)
editor.apply()
}
fun getToken(context: Context):String?{
val prefs:SharedPreferences=context.getSharedPreferences(USER_INFO,
Context.MODE_PRIVATE)
return prefs.getString(this.USER_TOKEN,null)
}
fun clearData(context:Context){
val editor=context.getSharedPreferences(USER_INFO,Context.MODE_PRIVATE).edit()
editor.clear()
}
}
LoginActivity.kt
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private val viewModel by viewModels<LoginViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding=ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
var id=binding.idBox.text
var pw=binding.pwBox.text
val token=TokenManager.getToken(this)
if(!token.isNullOrBlank()){
// 로그인 실패 알림(ui 어케할지 질문->그냥 toast알람?)
}
viewModel.loginResult.observe(this){
when(it){
is BaseResponse.Loading->{
// 기다려주세요 메시지?로고?
}
is BaseResponse.Success->{
processLogin(it.data)
}
is BaseResponse.Error->{
processError(it.msg)
}
else->{
//loading 종료시
}
}
}
binding.loginBtn.setOnClickListener {
doLogin()
}
}
fun doLogin(){
val email=binding.idBox.text.toString()
val password=binding.pwBox.text.toString()
viewModel.loginUser(email,password)
}
fun processLogin(data:LoginResponse?){
showToast("success:"+data?.message)
if(!data?.result?.accessToken.isNullOrEmpty()){
data?.result?.accessToken.let{
TokenManager.saveToken(this,it!!)
}
//로그인 다음화면으로 navigation
}
}
fun processError(msg:String?){
showToast("error:"+msg)
}
fun showToast(msg:String){
Toast.makeText(this,msg,Toast.LENGTH_SHORT).show()
}
}
//LoginViewModel
val loginResult: MutableLiveData<BaseResponse<LoginResponse>> = MutableLiveData()
loginViewModel에서 선언했던 live data인 loginResult를 observe합니다 →저 데이터에 변화가 있는지 확인합니다
viewModel.loginResult.observe(this){
when(it){
is BaseResponse.Loading->{
// 기다려주세요 메시지?로고?
}
is BaseResponse.Success->{
processLogin(it.data)
}
is BaseResponse.Error->{
processError(it.msg)
}
else->{
//loading 종료시
}
}
}
loginBtn이 클릭시에는 doLogin()이 실행됩니다
viewModel.loginUser()를 실행시키네영
fun doLogin(){
val email=binding.idBox.text.toString()
val password=binding.pwBox.text.toString()
viewModel.loginUser(email,password)
//viewModelScope.launch를 통해 코루틴 실행시키고 api request 요청해서
//코드에 따른 결과값 처리했던 코드 !!
}
fun processLogin(data:LoginResponse?){
showToast("success:"+data?.message)
if(!data?.result?.accessToken.isNullOrEmpty()){
data?.result?.accessToken.let{
TokenManager.saveToken(this,it!!)
}
//로그인 다음화면으로 navigation
}
}

구어체 주의9️⃣
LoginBtn을 클릭시 doLogin()이 실행
doLogin()에서는 viewModel.loginUser를 실행하고
그 안에있는 viewModelScope.launch에서 코루틴을 실행해
LoginActivity에서는 선언한 viewModel에 선언된 MutableLiveData인 loginResult의 변화를
viewModel.loginResult.observe를 통해 확인해
그렇게 loginResult의 상태가 BaseResponse.Loading,Success,Error인지 따라 다른 함수를 실행해
그중 Success일때는 processLogin()를 통해 data의 결과값에서 token을 받기+저장
참고 🤗
[Android] MVVM + AAC + FireBase Google Login #1
전체 구현 과정 참고 사이트
Login API with retrofit and MVVM with auto-login in android kotlin.
'안드로이드 > 프로젝트 개발 구현' 카테고리의 다른 글
| data binding(with liveData, viewModel) (1) | 2024.03.31 |
|---|---|
| 소셜 로그인 카카오 (0) | 2024.03.31 |
| auth, anonymous retrofit(서로다른 interceptor붙은 retrofit 객체 생성) (0) | 2024.03.31 |
| 로그인 access-token 관리(token header 추가,만료된 token 재발급) (1) | 2024.03.31 |
| 클린 아키텍쳐, 의존성 주입(사용자 프로필 조회 예시) (0) | 2024.03.31 |