발견록_06

김재현·2023년 5월 22일
0

안드로이드

목록 보기
9/12

1. retrofit2 + coroutine사용하기

기존에는 retrofit2를 사용해서 api와 통신할때

fun getTopBnrLists(){
        service.getTopBnrData().enqueue(object: Callback<TopBnrDataList> {
            override fun onResponse( call: Call<TopBnrDataList>, response: Response<TopBnrDataList>) {
                if(response.isSuccessful){
                    response.body()?.let{
                        _topBnrLists.value = it.TopBannerList
                    }
                }
            }
            override fun onFailure(call: Call<TopBnrDataList>, t: Throwable) {
                Log.d("viewbannerlist","${t.toString()}")
            }
        })
    }

이런 방식으로 ViewModel에서 사용했었다.
이때는 interface에서 call을 통해 데이터를 불러왔을때 사용하는 방법이었다.

interface RetrofitService {
    @GET("RqBannerList")
    fun getTopBnrData(): Call<TopBnrDataList>
}

그런데 ViewModel에서 enque말고 coroutine을 사용하자면 어떻게해야할까?
일단 interface의 Call부분을 Response로 바꾸고 suspend함수로 바꾼후에, ViewModel에선 enque를 사용하지 않는다.

interface RetrofitService {
    @GET("RqBannerList")
    suspend fun getTopBnrData(): Response<TopBnrDataList>
fun getTopBnrLists(){
        viewModelScope.launch {
            val response = service.getTopBnrData()
            if (response.isSuccessful){
                response.body()?.let{
//                    _topBnrLists.value = it.TopBannerList
                    _topBnrLists.emit(it.TopBannerList)
                    Log.d("태그", "getTopBnrLists: $it")
                }
            }
        }
    }

ViewModel에서는 viewModelScope를 통해서, interface에서 만들었던 함수와 retrofit Manager와 연결한 부분은 그대로 사용하고 response를 통해서 데이터를 받아올 수 있다.

여기서 추가로 기존에 LiveData에 넣었던 방식인 .value가 아니라, emit을 통해서 flow로 데이터를 넘겨줄 수 있는데

private var _topBnrLists = MutableSharedFlow<ArrayList<TopBnrData>>()
    val topBnrLists: SharedFlow<ArrayList<TopBnrData>> = _topBnrLists

이때는 다음과 같이 MutableSharedFlow자료형을 사용해야한다. 아니면 MutableStateFlow나 MutableFlow를 선택할 수 있는데 MutableStateFlow는 초기값을 지정해주는등 서로 다른 부분이 있기 때문에 잘 골라서 사용하면 되는 것 같다.

그 후 MainActivity에서는 lifecycleScope로 생명주기를 관리해주고 repeatOnLifecycle과 Lifecycle.State.STARTED를 사용하여 비동기 통신이 언제 이루어져야 하는지 관리해준다. flow로 ViewModel에서 데이터를 emit으로 담은건 collect{}를 사용하여 받아올 수 있다.


2. 리사이클러뷰, 뷰페이저2의 Adapter 관리하기

ViewModel에서 API를 통신하는 과정은 observe등으로 MainActivity에서 게속 지켜보고있다고 치면, observe안에서 adapter와 연결하는 과정을 넣는다면 비효율?적일 수 있다.

기존의 collect{}안에서 adapter연결한 부분을 빼고, adapter의 매개변수로 리스트주는것도 빼는 방식을 사용하였다.

class TopBnrAdapter()
    : RecyclerView.Adapter<TopBnrAdapter.CustomViewHolder>() {

    var item: ArrayList<TopBnrData> = ArrayList()
    
    override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
        if(item.isNotEmpty()){
            holder.bind(item[position % item.size])
        }
    }
    
    fun initializeItem(data: ArrayList<TopBnrData>){
        item = data
        Log.d("태그", "initializeItem: $item")
        notifyDataSetChanged()
    }

주로 바뀌는 부분만 작성하였다. TopBnrAdapter()안에 매개변수를 없애고 프로퍼티로 따로 만들어주었다.
initializeItem함수를 사용하여 외부에서 item을 초기화해주는 방식을 사용하여 초기화 시켜보자.

private lateinit var topBnrAdapter: TopBnrAdapter

private fun mainViewModelFlow(){
        lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED){
                launch {
                    mainViewModel.topBnrLists.collect{
                        topBnrAdapter.initializeItem(it)
                        }...
                        
private fun topBnrAdapterConnect(){
        topBnrAdapter = TopBnrAdapter()
        binding.topbnrViewPager.apply{
//            setCurrentItem(Int.MAX_VALUE / 2 - (Int.MAX_VALUE / 2) % topBnrAdapter.item.size, false)
            adapter = topBnrAdapter
            
            registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {                        
                       ...

처럼 MainActivity에서 함수를 분리해서 적을 수 있다. 기존에는 topbnrAdapterConnect()부분이 collect{}안에 들어가 있었지만, 이제는 collect{}안에는 topBnrAdapter의 initializeItem함수를 사용하여 초기화해주는 부분만으로 작성하고 topBnrAdapterConnect()함수에서는 정말로 어뎁터와 연결해주는 부분을 적으면 된다.

이 경우의 문제점이라고 한다면 topBnrAdapterConnect에서 setCurrentItem을 한다고 item의 size를 구할 수 없다는 문제가 있다.

왜 그런지는 아직 모르겠지만 onCreate()안에서 두 함수를 순서없이 적어도 무조건 TopBnrAdapter()가 만들어진다. 그렇기 때문에 topBnrAdapter의 initializeItem이 되지 않았기 때문에 item은 빈리스트가 만들어지고 Adapter함수의 onBindViewHolder에서 isNotEmpty면 실행하라는 것과 같이 무조건 0이 먼저 실행되고 난 후에 initializeItem이 이후에 만들어진다.
즉, 비동기 과정이 나중에 실행되어 아직 데이터가 없다는 것이다. 이 점을 주의해야한다.

비동기 과정에 데이터가 없어 처음엔 0인 데이터가 들어가있지만 나중에 비동기 과정이 끝나서 초기화가 이루어지면 adapter의 onbindviewholder같은 함수가 다시 실행되는것같다. notifyDatasetChanged()를 실행해주면 명시적으로 다시 실행하지만 적지 않는다면 첫화면은 하얀색, 짧은 api호출 이후의 다음은 정상적으로 나오게 된다.

그런데 만약 api호출이 늦는다면 계속 하얀색 화면이 넘어가는것 같다.
또한 lifecycle.repeatOnLicycle(){}을 통해서 여러 launch를 통해 flow에서 collect()로 데이터를 받아오는데 순서는 실행할때마다 달라질 수 있는 것같다. 로그를 찍어본 결과 끝나는 시점이 불규칙한것을 볼 수 있었다.


3. Retrofit2 + flow 방식

2번에서 Retrofit 인터페이스를 통해서 데이터를 반환하는 형식이 Response<Data>형식이였다.

interface RetrofitService {
    @GET("RqBannerList")
    suspend fun getTopBnrData(): TopBnrDataList
//    suspend fun getTopBnrData(): Response<TopBnrDataList>
//    fun getTopBnrData(): Call<TopBnrDataList>

chatgpt답변. 맹신은 하지 말자

Call<>로 주면 retrofit의 enque로 비동기 처리를 할수 있고,
Response<>로 주면 데이터가 retrofit이 동기적으로 말들어져 body()로 넘어오기 때문에 이를 받아올 수 있었다.
하지만 원본 형식을 그냥 넘겨주게되면 이 때는 retrofit이 자동으로 비동기적인 방식으로 네트워크 요청을 처리하게 된다. 그래서 suspend함수로 호출해야한다.

그러면 그냥 ViewModel에서

lifecycleScope.launch {
    try {
        val topBnrData: TopBnrDataList = retrofitService.getTopBnrData()
        // 응답을 처리하는 로직 작성
    } catch (e: Exception) {
        // 에러 처리 로직 작성
    }
}

처럼 바로 데이터를 넘겨줄 수 있다.

그래도 flow를 통해서 데이터를 넘겨보는 것을 해보자.

Flow<TopBnrDataList> 형식으로 만들게 되면 gson이 형식을 인식하지 못했다는

 java.lang.RuntimeException: Unable to invoke no-args constructor for kotlinx.coroutines.flow.Flow<com.example.dallamain.data.TopBnrDataList>. Registering an InstanceCreator with Gson for this type may fix this problem

오류가 뜨게 된다. 왜냐하면 Retrofit에서 Flow는 'Flowable', 'Observable', 'Single'과는 다른 타입이기 때문에 지원하지 않기 때문이다. 이것에 대한 해결책이 있다고는 하는데 추가 구성이 필요하다고 한다. 그래서 이 방법으로는 하지 않기로 했다.

참고사이트 <- 이 사이트를 참고하여 만든 방식이다.

전체적인 흐름은 Retrofit으로 받아온 데이터를 flow로 만든 함수를 통해 emit을 해주는 것이다. 그럼 ViewModel에서 emit을 한것을 받아서 MutableSharedFlow나 MutableStateFlow등에 collect를 통해 emit으로 다시 넣어준다. 그럼 MainActivity에서 collect로 받아올 수 있다. 즉, 2번 넘어간다는것이다(?)

한꺼번에 만들어도 되지만 코드의 구조화와 유연성을 높이기 위해, interface부분과 implementation(구현)부로 나눠서 만들었다.

interface ApiHelper {
    fun getTopBnrData(): Flow<Result<TopBnrDataList>>
    fun getFollowingData(): Flow<FollowingDataList>
    fun getEventData(): Flow<EventBannerDataList>
    fun getLiveSectionData(): Flow<LiveSectionDataList>
}

interface부분으로 함수 이름과 반환형식만 지정해주었다.

실제 구현부로 interface를 상속받아서 구현하게 되어있다. 우리가 만들었던 retrofitService를 매개변수로 받아서 우리가 반환형식으로 지정해둔 원본 데이터 TopBnrDataList등의 형식으로 가져오는 것이다.

만들다보니 override fun getTopBnrData()retrofitService.getTopBnrData()에서 함수 이름이 겹치는데 이 두 함수 이름은 전혀 상관이 없다. override fun 으로 만들고싶은 함수 이름은 interface부분에서 아무 이름이나 적어줘도 된다.

interface도 그렇고 구현부도 그렇고 두가지 형식으로 만들 수 있었는데 비동기 작업의 결과를 나타내는 Kotlin 라이브러리인 Result를 쓰는 방법과 쓰지 않는 방법이다. Result<T>의 value는 성공일 경우 T를 타입으로 하는 값을 가지게 되고 실패일 경우 Failure를 wrapper class로 하는 exception을 값으로 가지게 된다.

Result를 쓴다면 getTopBnrData()처럼, 안쓴다면 getFolloiwngData()처럼 쓰면 된다.

그럼 ViewModel에서는 이 클래스를 가져와서 emit한것을 collect하면 되는데

class MainViewModel(): ViewModel() {

    private val service: RetrofitService = RetrofitManager.retrofit
    val apiHelper = ApiHelperImpl(service)


이런 형식으로 collect를 해서 받아올 수가 있다.

profile
배운거 정리하기

0개의 댓글