저번 포스트에서 개발 준비를 했다. 모두가 다 했다고 생각해보고 가보자
자 다시 MVVM 패턴 그림을 보자
우린 여기서 Remote Data Source 쪽을 보자 Retrofit 을 통해 webService을 repostory 통해 요청이라 오케이 그럼 가보자
내가 해야할 작업은 저정도 이다. 많아 보이지만 절때 그렇지 않다
일단 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도 만들고 해보자....
(다음엔 일찍와서 코드 작성하고 블로그도 써야겠다....)
그럼 오늘도 읽어 주셔서 감사합니다.
-피드백와 비판은 언제나 환영입니다.-