문제해결 : LiveData와 Adapter의 sync 문제 (feat.coroutine)

2024. 8. 21. 10:57·Android/문제해결

⛔ 문제사항

 

문제의 화면이다.
 여행 채널 추천 카테고리를 보면 5개 밖에 출력되지 않은 현상을 볼 수 있다. 내가 의도한 것은 총 6개의 채널이 추천되며 그리드뷰가 꽉 채워지는 것이었는데 다른 화면을 이동하거나, 앱을 재실행하여 해당 Fragment가 다시 그려질 때마다 채널이 2개부터 6개까지 그려지는 개수가 그때그때 다르다는 것이었다. 그러나 아래 스크린샷과 같이 로그캣을 찍어보면 itemList의 총 개수는 6개로 잘 넘어오는 것을 알 수 있었다.

 

당시 코드다. channelList는 LiveData로 관리하고 있고, channel을 가져오는 api는 async await을 통해 비동기적으로 받아오고 있었다.

class HomeViewModel(
    private val channelRepository: ChannelRepository = ChannelRepositoryImpl()
) : ViewModel() {
    private val idList = listOf(
        "@JBKWAK", "@PaniBottle", "@im1G", "@soy_the_world", "@tripcompany93", "@jojocamping", "@CHOMAD", "@Birdmoi", "@kimhanryang97", "@YongZinCamp",
        "@sookoh수코", "@CHACHABBO_VLOG", "@nanajane", "@DarongT", "@koreanjay", "@korea_travel", "@chabakchabak", "@awesomebackpakers", "@Birdmoi", "@_davidghc",
    )

    private val _channelList = MutableLiveData<List<ChannelListModel>>()
    val channelList: LiveData<List<ChannelListModel>> = _channelList

    fun fetchChannel() {
        val newIdList = idList.shuffled().slice(0..5)
        val currentList = _channelList.value?.toMutableList() ?: mutableListOf()

        newIdList.forEach { it ->
            viewModelScope.launch {
                runCatching {
                    val fetchResult = async { return@async channelRepository.getChannel(it) }
                    val result = fetchResult.await()
                    currentList.add(result.toChannelListData())
                    _channelList.value = currentList
                }.onFailure {
                    Log.e("💡HomeViewModel fetchChannel", "fetchChannel() onFailure: ${it.message}")
                }
            }
        }
    }

    fun clearList() {
        _channelList.value = listOf()
    }
}

 

✅ 해결방안

 이러한 버그가 발생한 원인으로는 바로, LiveData가 Adapter와 sync가 맞지 않아서였다.

 fetchChannel 함수 내부의 forEach문을 돌면서 channelRepository.getChannel(it)이 호출될 때마다 LiveData가 바뀌고, 이 LiveData가 바뀔 때마다 Adapter를 다시 그리면서 타이밍이 어그러져 발생하는 문제였다.

 

1. Job을 이용한 해결방안

 Job을 별도로 선언하여 forEach문이 돌 때마다 job을 추가해주고, 반복문 실행이 완료되면 jobs.joinAll() 해주어 LiveData에 안전하게 반영해주는 방법이 있다.

class HomeViewModel(
    private val channelRepository: ChannelRepository = ChannelRepositoryImpl()
) : ViewModel() {
    private val idList = listOf(
        "@JBKWAK", "@PaniBottle", "@im1G", "@soy_the_world", "@tripcompany93", "@jojocamping", "@CHOMAD", "@Birdmoi", "@kimhanryang97", "@YongZinCamp",
        "@sookoh수코", "@CHACHABBO_VLOG", "@nanajane", "@DarongT", "@koreanjay", "@korea_travel", "@chabakchabak", "@awesomebackpakers", "@Birdmoi", "@_davidghc",
    )

    private val _channelList = MutableLiveData<List<ChannelListModel>>()
    val channelList: LiveData<List<ChannelListModel>> = _channelList
    
    val jobs = mutableListOf<Job>()

    fun fetchChannel() {
        val newIdList = idList.shuffled().slice(0..5)
        val currentList = _channelList.value?.toMutableList() ?: mutableListOf()

 		newIdList.forEach { it ->
            val job = viewModelScope.launch {
                runCatching {
                    val fetchResult = async { return@async channelRepository.getChannel(it) }
                    val result = fetchResult.await()
                    currentList.add(result.toChannelListData())
                    _channelList.value = currentList
                }.onFailure {
                    Log.e("💡HomeViewModel fetchChannel", "fetchChannel() onFailure: ${it.message}")
                }
            }

            jobs.add(job)
        }
        
        
        jobs.add(job)
        
        viewModelScope.launch {
            jobs.joinAll()
            _channelList.value = currentList
        }
    }

    fun clearList() {
        _channelList.value = listOf()
    }
}

 

2. UseCase를 이용한 해결방안

channelRepository.getChannel(it) 함수 호출을 UseCase로 분리하여 서버에서 가져오는 데이터를 LiveData와 아예 분리시키는 해결방안이 있다.

class HomeViewModel(
    private val channelRepository: ChannelRepository = ChannelRepositoryImpl(),
    private val channelUseCase: ChannelUseCase = ChannelUseCase(ChannelRepositoryImpl())
) : ViewModel() {
    private val idList = listOf(
        "@JBKWAK", "@PaniBottle", "@im1G", "@soy_the_world", "@tripcompany93", "@jojocamping", "@CHOMAD", "@Birdmoi", "@kimhanryang97", "@YongZinCamp",
        "@sookoh수코", "@CHACHABBO_VLOG", "@nanajane", "@DarongT", "@koreanjay", "@korea_travel", "@chabakchabak", "@awesomebackpakers", "@Birdmoi", "@_davidghc",
    )

    private val _channelList = MutableLiveData<List<ChannelListModel>>()
    val channelList: LiveData<List<ChannelListModel>> = _channelList
    
    val jobs = mutableListOf<Job>()

    fun fetchChannel() {
        val newIdList = idList.shuffled().slice(0..5)
        
        viewModelScope.launch {
            val list = channelUseCase.getChannelList(newIdList)
            _channelList.value = list
        }
    }

    fun clearList() {
        _channelList.value = listOf()
    }
}
class ChannelUseCase(
    private val channelRepository: ChannelRepository,
) {
    suspend fun getChannelList(idList: List<String>): List<ChannelListModel> {
        var list: MutableList<ChannelListModel> = mutableListOf()
        for (i in idList.indices) {
            list.add(channelRepository.getChannel(idList[i]).toChannelListData())
        }
        return list
    }
}

 이렇게 UseCase로 분리하면 화면에 직접 영향을 주는 LiveData가 실제 데이터 List<ChannleListModel>로 분리되면서 보다 안전하게 값을 가져올 수 있으며, 비즈니스 로직을 ViewModel로부터 분리하여 효율적으로 코드 관리가 가능하다는 점에서 좀 더 유용하다.

 그럼 이렇게 내가 원하는 대로 총 6개의 채널이 어떤 상황에서도 제대로 보여진다!

 

❗출처

튜터님의 아름다운 코칭

저작자표시 비영리 (새창열림)

'Android > 문제해결' 카테고리의 다른 글

문제해결 : Supplied auth credential is incorrect, malformed or has expired (feat. ActivityResultLauncher)  (3) 2024.09.07
문제해결 : Error: com.google.android.gms.common.api.ApiException: 10  (4) 2024.08.31
문제해결 : Inconsistency detected. Invalid view holder adapter positionHolder  (0) 2024.08.09
문제해결 : Unresolved reference: BuildConfig  (0) 2024.08.01
문제해결 : Val cannot be reassigned  (0) 2024.07.29
'Android/문제해결' 카테고리의 다른 글
  • 문제해결 : Supplied auth credential is incorrect, malformed or has expired (feat. ActivityResultLauncher)
  • 문제해결 : Error: com.google.android.gms.common.api.ApiException: 10
  • 문제해결 : Inconsistency detected. Invalid view holder adapter positionHolder
  • 문제해결 : Unresolved reference: BuildConfig
깨비도
깨비도
그림 그리는 개발자의 인디게임 생존기 & Flutter 연구소
  • 깨비도
    KKEVi.log()
    깨비도
  • 전체
    오늘
    어제
    • 전체 (98) N
      • 인디게임 개발일지 (6) N
      • C# (1)
      • Dart (3)
      • Flutter (24)
        • 문제해결 (14)
      • Kotlin (12)
      • Android (22)
        • 문제해결 (11)
      • CS (10)
        • Network (1)
      • 알고리즘 (10)
        • 코딩테스트 (10)
      • etc (10)
        • Git (1)
        • React (1)
  • 블로그 메뉴

    • 방명록
  • 링크

    • 그림 전문 일지
  • 공지사항

  • 인기 글

  • 태그

    플랫포머_배경
    MacOS
    XML
    thread
    네트워크
    CS
    IOS
    context
    인디게임개발
    게임아트
    stack
    인디게임
    게임기획
    Android
    C#
    DartVM
    Kotlin
    플러터
    flutter
    유니티
    DART
    게임개발
    Unity이펙트
    Gemini
    Firebase
    when
    ram
    Dear.MyMarionette
    2D아트워크
    OS
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.5
깨비도
문제해결 : LiveData와 Adapter의 sync 문제 (feat.coroutine)
상단으로

티스토리툴바