‼️ 사전 작업 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 생성 과정

  1. LogginInterceptor생성(통신과정 로그 출력시 유용)
  2. LogginInterceptor를 얹어서 OkHttpClient 생성
  3. 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.

[안드로이드 스튜디오] Login Activity 템플릿 살펴보기

+ Recent posts