Naver Api mvvm 적용해보기

Assist·2023년 5월 21일
0

Android

목록 보기
20/21

저번 포스트에서 개발 준비를 했다. 모두가 다 했다고 생각해보고 가보자

자 다시 MVVM 패턴 그림을 보자

우린 여기서 Remote Data Source 쪽을 보자 Retrofit 을 통해 webService을 repostory 통해 요청이라 오케이 그럼 가보자

  • retrofit 클래스 개발
  • webService 클래스 개발
  • repository 클래스 개발
  • viewModel 개발

내가 해야할 작업은 저정도 이다. 많아 보이지만 절때 그렇지 않다

일단 retrofit 클래스 개발

object  RetrofitInterface {

    //요청후 30초동안 안오면 에러 처리 위함 , 그리고 Logcat 에 Log 을 보기 위해 
    private val okHttpClient : OkHttpClient by lazy {
        OkHttpClient.Builder()
            .addInterceptor(HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            })
            .readTimeout(30 , TimeUnit.SECONDS)
            .writeTimeout(30 , TimeUnit.SECONDS)
            .build()
    }

 
    private var instance : HttpsServices ?= null


    fun retrofitInstance() : HttpsServices {
        if(instance == null) {
            instance = Retrofit.Builder()
                .baseUrl(BuildConfig.BASE_URL_JSON)
                .addConverterFactory(GsonConverterFactory.create())
                .client(okHttpClient)
                .build()
                .create(HttpsServices ::class.java)
        }
        return instance!!
    }

}

막상 별개 없다 그저 instance으로 싱글톤 패턴으로 구현했다.
왜 싱글톤이냐? 필자는 웹을 1도 모르지만 안드로이드 핸드폰이 pc보다 좋다고 볼수 있을까?
필자는 절때 아니라는게 내 정론이다. 그러므로 instance가 한번만 생성될수 있도록 또한 instance가 함부로 접근 하지 못하게 하는게 내 방법이다.

자 그럼 webService 클래스를 만들어보자

interface HttpsServices {
	
    /**
     * 책 요청 api 
     */
    @GET("book.json")
    fun requestBookApi(
        @Header("X-Naver-Client-Id") id : String ,
        @Header("X-Naver-Client-Secret") pw : String,
        @Query("query") query : String) : Call<Any>

}

이런식으로 정리하면 한곳에 요청함수를 정리할수 있음으로 깔끔해 보인다. 우리가 지금 검색 api 을 사용하고 있다
이뜻은 영화 블로그 etc 기능 추가될때마다 HttpsService 에 추가하면 끝이다. 편리하죠?

자그럼 repostory 클래스도 만들어보자

class Repository {
    //책 API
    fun requestBookApi(id : String , pw : String , query : String) = RetrofitInterface.requestRetrofit().requestBookApi(id , pw , query)

}

진짜 별거 없다. viewModel 에서 Repository 변수를 매게변수로 넘겨줘서 요청만 하면 된다
단 그전에 필자가 저번 포스트에서 si회사에서 썼던 클래스를 잠시 만들고 가자

data class Resource<out T>(val status: Status, val data: T?, val exception: Throwable?, val newPage: Int?, val FailMessage:String?, val title: String? , val message: String?) {

    enum class Status {
        LOADING,
        SUCCESS,
        ERROR,
        FaIL
    }

    companion object {
        fun <T> success(data: T?): Resource<T> {
            return Resource(Status.SUCCESS, data,null, null , null,null , null )
        }

        fun <T> error(exception: Throwable?): Resource<T> {
            return Resource(Status.ERROR, null, exception, null, null,null , null )
        }

        fun <T> loading(): Resource<T> {
            return Resource(Status.LOADING, null, null, null, null,null, null )
        }
        fun <T> fail(message : String) : Resource<T>{
            return Resource(Status.FaIL, null, null, null, null,null, message )
        }

    }
}

이걸 어따 쓰냐? viewModel에서 LiveData쓸때 보니까 한번 사용해보도록 하자

class SearchViewModel ( private val repository : Repository) : ViewModel() {

    private val _bookSearchLiveData = MutableLiveData<Resource<String>>()
    val bookSearchLiveData get() = _bookSearchLiveData

    /**
     * 책 검색 api
     */
    fun requestBookApi(id :String , pw : String , query : String) = viewModelScope.launch(Dispatchers.IO) {
        _bookSearchLiveData.postValue(Resource.loading())

        val requestBookApi = repository.requestBookApi(id ,pw , query)
        requestBookApi.enqueue(object: Callback<Any> {
            override fun onResponse(call: Call<Any>, response: Response<Any>) {
                if(response.isSuccessful){
                    when(response.code()){
                        Define.SYSTEM_ERROR_NO_API ->{
                            _bookSearchLiveData.postValue(Resource.error(null))
                            return
                        }
                        Define.SYSTEM_ERROR_WRONG_QUERY ->{
                            _bookSearchLiveData.postValue(Resource.fail(Define.MESSAGE_NO_QUERY))
                            return
                        }
                        Define.SYSTEM_ERROR ->{
                            _bookSearchLiveData.postValue(Resource.fail(Define.MESSAGE_SYSTEM_ERROR))
                        }
                        Define.SYSTEM_SUCCESS->{
                            _bookSearchLiveData.postValue(Resource.success("Success"))
                        }
                    }
                    return
                }
                _bookSearchLiveData.postValue(Resource.error(null))
            }

            override fun onFailure(call: Call<Any>, t: Throwable) {
                _bookSearchLiveData.postValue(Resource.error(t))
            }
        })
    }



}

자 ViewModel은 정보를 저장하고 정보가 변화하면 view에 알려준다 근데 Resource라는 클래스를 씀으로 인하여 코드 가독성이 좋아졌다. 서버로 부터 응답코드에 따라 데이터 가져오기 성공 실패 로 나눌수 있다. 아직 구미가 안당기겠찌...나도그랬어...

class MainActivity : AppCompatActivity() {
    private val Tag =  "MainActivity"
    val repository  = Repository()

    private var _viewModel : SearchViewModel ?= null
    private val viewModel get()= _viewModel!!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        init()
        addListener()
    }
	//vieModel한테 repostory을 매개변수로 넘겨주고 vieModel 생명주기를 activity로 잡는것 
    private fun init(){
        val factory = SearchViewModelFactory(repository)
        _viewModel = ViewModelProvider(this , factory)[SearchViewModel :: class.java]
    }

    private fun addListener(){

        viewModel.bookSearchLiveData.observe(this , Observer {
            when(it.status){
                Resource.Status.LOADING ->{
                    Log.d(Tag , "Loading for Book Search ")
                }
                Resource.Status.SUCCESS ->{
                    Log.d(Tag , "Success to get Book API")
                }
                Resource.Status.FaIL ->{
                    Log.d(Tag,  "Fail to get BookApi")
                    Toast.makeText(this , it.message , Toast.LENGTH_SHORT).show()
                }
                Resource.Status.ERROR->{
                    Log.e(Tag,  "Error Book Api")
                    Toast.makeText(this , "네트워크를 확인해 주세요" , Toast.LENGTH_SHORT).show()
                }
            }
        })
    }


}

어떠냐 addListener을 보면 viewModel이 Loading success , fail, error 때마다 분기처리를 할수 있다.
잠시 google의 viewModel 예제를 볼까?
https://developer.android.com/topic/libraries/architecture/livedata?hl=ko
가서 읽어보면 이게 뭔소리여... 라는 생각을 한다. 필자도 그런 생각을 했다. 보면 String으로 간단하게 만 데이터 변화를
보여줄수 있다,. 하 지 만 Resource을 쓰면 성공 실패 로딩 다 알수 있으며 심지어 view에서도 각 분기마다 view 갱신을 해줄수 있다. 아주 유용한 data class이다. ~,~

자그럼 이제 기능도 만들어 봤으니 다음 시간엔 fragment에 viewModel을 써서 view도 만들고 해보자....
(다음엔 일찍와서 코드 작성하고 블로그도 써야겠다....)

그럼 오늘도 읽어 주셔서 감사합니다.
-피드백와 비판은 언제나 환영입니다.-

profile
안드로이드만 좋아하는 특이한 개발자

0개의 댓글