반응형

이번 게시물은 Retrofit에 ViewModel을 적용해보았다.

단순하게 Retrofit을 ViewModel에서 사용하면 되는줄 알았는데, "MVVM패턴"을 적용하면서 개발하려 하니 넣어야 할 것들이 생각보다 많았다........

그래서, 혼자 공부한 내용인 Retrofit + ViewModel + Coroutine을 종합해서 날씨정보를 불러오는 어플을 만들어보았다.

아래 게시물들에 대한 이해가 있다면 이해하기 편하다.

 

[안드로이드 스튜디오 독학#27] 현재 날씨 어플

Thread와 Network를 공부하고 개념을 정리하기 전에 간단한 예제를 구현하려고 했는데, 생각지 못한 에러가 여러곳에서 발생했고, 이를 해결하다보니 게시물을 올리는 시간이 늦춰졌다. 스레드를

seminzzang.tistory.com

 

[안드로이드 스튜디오 독학#28] 현재 날씨 어플2

현재 날씨 어플을 AsyncTask를 통해 구현한 후, 같은 기능을 Retrofit을 이용해 RESTful API를 구현해봤다. 어플의 기능은 아래 링크와 같다. [안드로이드 스튜디오 독학#27] 현재 날씨 어플 Thread와 Network

seminzzang.tistory.com

1. Repository

저번 게시물에서 RecyclerView를 사용했을 때는 Model을 따로 구현하지 않았다.

그랬기 때문에 ViewModel내의 LiveData에서 모든 데이터를 처리했고, Repository의 개념이 필요하지 않았었다.

Repository.... MVVM을 공부하면서 Repository의 존재에 대해 알긴했는데, 그냥 넘어갔던것 같다.

Repository는 아래 이미지를 보면 이해가 잘 될것이다.

Repository를 쉽게 설명하자면 ViewModel에서 데이터를 가져올 때 DB(Room)에서 가져오거나 네트워크(Retrofit)에서 가져오거나 동일한 모델을 제공하도록 하는 것이다.

즉, ViewModel과 데이터에 엑세스 하는 로직을 분리하는 것이다.

구현한 Repository 코드는 아래와 같다.

MainRepository.kt

1
2
3
4
5
6
7
package com.example.retrofitexamplekt
 
import com.example.retrofitexamplekt.data.RetrofitItem
 
interface MainRepository {
    suspend fun getAPI(): RetrofitItem
}
cs

MainRepositoryImpl.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.retrofitexamplekt
 
import android.util.Log
import com.example.retrofitexamplekt.data.RetrofitItem
 
object MainRepositoryImpl :MainRepository{
    var str:String = ""
    private val serviceKey = "My Key";
 
 
    override suspend fun getAPI(): RetrofitItem{
        val data = RetrofitClient().getRetrofitInterface().getData(serviceKey,10000,"JSON","20220203","0600",57,122)
        Log.e("MainRepositoryImpl : ", data.isSuccessful.toString())
        return data.body()!!
    }
}
cs

Repository를 작성하는 것은 다른사람들의 블로그를 여러군데 봤는데, 정해진 작성방법 같은건 없고 Room의 데이터나 Retrofit을 사용한 데이터를 전달만 잘 해주면 된다.

그래서 직관적으로 볼수있게 interface를 만들어서 object에서 override해서 사용할 수 있게 구현해놨다.

끝난건가....

그런데 또 문제가 생겼다. Coroutine에 대해 이해하고 있는 사람이라면 알겠지만, 'suspend'라는 키워드가 Repository의 함수에 붙어있다.

그렇다..... 비동기 처리를 위해 Coroutine을 사용한 것이다.

비동기 처리를 왜 해줘야 되는지는 Retrofit 코드를 보면 알수있다.

 

2. Retrofit

이전에 Retrofit을 사용했을 때는 비동기 처리를 별도로 해주지 않았다.

왜냐면 Retrofit의 반환 값을 Call 객체를 통해 받았기 때문이다.

Call 객체를 사용할 경우 enqueue callback 안의 onResponse()와 onFailure()에서 결과에 대한 작업을 처리해주었다. 

하지만 우리는 Repository를 사용하면서 ViewModel로 하여금 반환되는 데이터의 출처를 모르게 해야할 의무가 있다.

즉, Repository에서 Call객체를 사용하는 것이 아닌, Response객체를 사용해야한다.

왜냐하면 Call객체를 받으면 response를 enqueue callback 에서 사용해야 되기 때문!

Retrofit에서 Response객체를 받아서 Repository에서 Response.body를 반환한다면 ViewModel에서 데이터의 출처를 모르게 할 수 있을것이다!

RetrofitClient.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.retrofitexamplekt
 
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
 
class RetrofitClient {
    private val retrofitInterface: RetrofitInterface
    val baseUrl: String = "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/"
 
    init {
        var retrofit = Retrofit.Builder()
            .baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        retrofitInterface = retrofit.create(RetrofitInterface::class.java)
    }
 
    fun getRetrofitInterface(): RetrofitInterface = retrofitInterface
 
}
cs

RetrofitInterface.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.retrofitexamplekt
 
import com.example.retrofitexamplekt.data.RetrofitItem
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query
 
interface RetrofitInterface {
    @GET("getUltraSrtNcst")
    suspend fun getData(@Query("serviceKey") serviceKey:String,
                @Query("numOfRows") numOfRows:Int,
                @Query("dataType") dataType:String,
                @Query("base_date") base_date:String,
                @Query("base_time") base_time:String,
                @Query("nx") nx:Int,
                @Query("ny") ny:Int
    ): Response<RetrofitItem>
}
cs

다른 게시물들에서 작성했던 Retrofit의 코드와 유사하다.

다만 다른점이 있다면 Interface에서 반환값을 Response<> 형식으로 지정해주었다.

 

3. Coroutine

Retrofit에서 Response객체를 사용할 때 비동기 처리를 해주어야 된다고 한다.

다른 사람들이 그렇게 말해서 그렇게 알고있긴 하지만, 이부분은 시간이 난다면 더 찾아봐야겠다....

이를 위해 사용한 것이 suspend, viewModelScope이다.

MainViewModel.kt

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
32
33
package com.example.retrofitexamplekt
 
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.retrofitexamplekt.data.RetrofitItem
import kotlinx.coroutines.launch
import java.lang.Exception
 
class MainViewModel() : ViewModel(){
    val data:MutableLiveData<RetrofitItem> = MutableLiveData()
    private val _str = MutableLiveData<String>("응답 없음")
    val str:LiveData<String> get() = _str
 
    init{
        Log.e("MainViewModel : ", data.value?.response?.body?.dataType.toString())
        loadData()
        Log.e("MainViewModel : ", data.value?.response?.body?.dataType.toString())
 
    }
 
    fun loadData(){
        viewModelScope.launch{
            try{
                data.value = MainRepositoryImpl.getAPI()
            }catch(e: Exception){
 
            }
        }
    }
}
cs

MainActivity.kt

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.example.retrofitexamplekt
 
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import com.example.retrofitexamplekt.databinding.ActivityMainBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.*
 
class MainActivity : AppCompatActivity() {
    private lateinit var mBinding: ActivityMainBinding
    private val binding get() = mBinding!!
 
    private val viewModel: MainViewModel by viewModels()
    private val serviceKey = "서비스키";
 
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
 
        binding.mainViewModel = viewModel
 
 
        initObserver()
    }
 
 
    fun initObserver(){
        viewModel.data.observe(this, androidx.lifecycle.Observer {
            binding.tvTest.text = "SUCCESS"
 
            binding.tvTest2.text  = "PTV : " + it?.response?.body?.items?.item?.get(0)?.obsrValue + '\n' +
                    "REH : " + it?.response?.body?.items?.item?.get(1)?.obsrValue + '\n' +
                    "R1N : " + it?.response?.body?.items?.item?.get(2)?.obsrValue + '\n' +
                    "T1H : " + it?.response?.body?.items?.item?.get(3)?.obsrValue + '\n' +
                    "UUU : " + it?.response?.body?.items?.item?.get(4)?.obsrValue + '\n' +
                    "VEC : " + it?.response?.body?.items?.item?.get(5)?.obsrValue + '\n' +
                    "VVV : " + it?.response?.body?.items?.item?.get(6)?.obsrValue + '\n' +
                    "WSD : " + it?.response?.body?.items?.item?.get(7)?.obsrValue
        })
    }
cs

ViewModel의 init에서 데이터를 불러왔고, activity에서 옵저버를 통해 값을 TextView에 넣어줬다.

코루틴에 대한 내용은 추후에 게시물로 작성할 예정이니 별도의 설명은 안하도록 하겠다.

 

느낀점

1. 데이터를 읽어오는것 뿐만 아니라 지정해주는것도 ViewModel에서 설정해주고 싶었는데, 잘 되지 않았다. 더 공부를 해보고 ViewModel에서 옵저버를 호출해서 데이터를 넣어주는 것도 찾아봐야겠다.

2. 사실 알고있긴 했지만 이번 기회에 Log를 처음 사용해봤는데 생각보다 많이 유용한 기능인것같다. 자주 사용하도록 해야겠다.

3. 이번 게시물까지는 패키지 구조를 고려하지 않고 어플을 만들었는데, MVVM에 대해 이것저것 찾아보면서 패키지 구조, Clean Architecture에 대한 것도 새로 알게되어서 멀티모듈을 사용하는 것까지 할수있으면 해봐야겠다.

반응형

+ Recent posts