일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 안드로이드 유닛 테스트 예시
- android ar 개발
- 안드로이드 라이선스
- 클래스
- 서비스 쓰레드 차이
- 멤버변수
- 큐 자바 코드
- jvm이란
- 자바 다형성
- rxjava disposable
- rxjava hot observable
- 스택 자바 코드
- 안드로이드 유닛 테스트
- 안드로이드 os 구조
- 2022 플러터 안드로이드 스튜디오
- 서비스 vs 쓰레드
- 플러터 설치 2022
- 안드로이드 레트로핏 사용법
- ar vr 차이
- 2022 플러터 설치
- jvm 작동 원리
- rxjava cold observable
- ANR이란
- 안드로이드 레트로핏 crud
- Rxjava Observable
- android retrofit login
- 안드로이드 라이선스 종류
- 객체
- 스택 큐 차이
- 안드로이드 유닛테스트란
- Today
- Total
나만을 위한 블로그
[Android] 페이징 라이브러리, Hilt, Flow로 Github API 사용하기 본문
이전 포스팅에선 Github API로부터 Flow로 데이터를 가져올 때 LiveData를 사용했지만 이번에는 Flow를 사용한 방식으로 리팩토링한 코드를 보인다. Flow를 제외한 Hilt와 페이징 라이브러리 설정은 동일하니 코드를 보고 싶다면 이전 포스팅을 확인하면 된다.
https://onlyfor-me-blog.tistory.com/725
위 포스팅에서 보인 코드 중 고칠 부분은 3가지다.
- GithubRepository
- GithubViewModel
- 액티비티 파일
Repository의 변경된 코드는 아래와 같다.
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GithubRepository @Inject constructor(
private val service: GithubApiService
) {
fun getSearchRepoResult(query: String): Flow<PagingData<Repo>> {
return Pager(
config = PagingConfig(
pageSize = NETWORK_PAGE_SIZE,
enablePlaceholders = false,
prefetchDistance = 50
),
pagingSourceFactory = { GithubPagingSource(service, query) }
).flow
}
}
getSearchRepoResult()의 리턴타입이 LiveData에서 Flow로 바뀌었고 flow 뒤의 .asLiveData()가 사라졌다. 추가로 prefetchDisatnce의 값이 50으로 변경된 사소한 변경점을 제외해도 그렇게 크게 바뀌진 않았다. 조금 바뀐 부분은 뷰모델과 액티비티다.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject
@HiltViewModel
class GithubViewModel @Inject constructor(
private val repository: GithubRepository
): ViewModel() {
private val _searchQuery = MutableStateFlow<String>("")
val repoResult: Flow<PagingData<Repo>> = _searchQuery
.flatMapLatest { query ->
if (query.isBlank()) {
flowOf(PagingData.empty())
} else {
repository.getSearchRepoResult(query).cachedIn(viewModelScope)
}
}
fun searchRepos(query: String) {
_searchQuery.value = query
}
}
Flow를 사용하는 만큼 LiveData를 다루기 위해 사용했던 MutableLiveData 대신 MutableStateFlow를 사용하고 생성자에 빈 문자열을 넘겨 초기화했다. SharedFlow를 사용하겠다면 아래 코드를 참고한다.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class GithubViewModel @Inject constructor(
private val repository: GithubRepository
): ViewModel() {
private val _searchQuery = MutableSharedFlow<String>(replay = 1)
val repoResult: Flow<PagingData<Repo>> = _searchQuery
.flatMapLatest { query ->
if (query.isBlank()) {
flowOf(PagingData.empty())
} else {
repository.getSearchRepoResult(query).cachedIn(viewModelScope)
}
}
fun searchRepos(query: String) {
viewModelScope.launch {
_searchQuery.emit(query)
}
}
}
차이라면 SharedFlow를 사용할 땐 searchRepos() 안에서 viewModelScope.launch {}를 사용했는데 StateFlow를 사용할 땐 쓰지 않았다. 이것은 StateFlow와 SharedFlow의 차이 때문이다.
- StateFlow : StateFlow는 상태를 저장하고, 새 값을 할당할 때 즉시 업데이트된다. 그래서 viewModelScope.launch {}를 사용할 필요 없이 값을 바꾸려면 그냥 value 프로퍼티만 할당하면 된다
- SharedFlow : 상태를 저장하지 않아서 값을 어딘가로 전달하려면 emit()을 호출해야 한다. 또한 코루틴 안에서 호출되어야 하기 때문에 viewModelScope.launch {} 안에서 코루틴을 시작한 다음 사용해야 한다.
리사이클러뷰에 데이터를 뿌려 UI를 실시간으로 업데이트해야 하기 때문에 나는 StateFlow를 사용했지만, 각각 다른 작업을 수행하는 구독자에게 이벤트를 전달해야 한다면 SharedFlow를 쓸 수 있겠다. 각자 상황에 알맞은 Flow를 사용한다.
그리고 flatMapLatest {}는 Flow의 값이 바뀔 때마다 새 Flow를 만들고 이전 Flow의 작업을 취소한다. 그래서 검색어가 바뀌면 이전 쿼리를 사용한 데이터 요청을 취소하고 새 검색어에 대한 데이터 요청을 시작해야 하기 때문에 flatMapLatest를 사용하면 알맞다. 그 외에는 똑같은 코드를 사용한다.
아래는 FlatMapLatest에 대한 코틀린 공식문서의 설명이다.
원래 Flow가 값을 내보낼 때마다 변환 함수에서 생성된 새 Flow로 전환하는 Flow를 반환한다. 원래 Flow가 새 값을 내보내면 변환 블록에서 생성된 이전 흐름이 취소된다
마지막으로 액티비티다.
@AndroidEntryPoint
class GithubPagingActivity :
BaseActivity<ActivityGithubPagingBinding>(R.layout.activity_github_paging) {
private val githubViewModel: GithubViewModel by viewModels()
private val repoListAdapter = RepoListAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bind {
viewModel = githubViewModel
lifecycleOwner = this@GithubPagingActivity
rvRepoList.apply {
addItemDecoration(
DividerItemDecoration(
this@GithubPagingActivity,
DividerItemDecoration.VERTICAL
)
)
adapter = repoListAdapter
}
etSearchQuery.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val query = etSearchQuery.text.toString()
if (query.isNotEmpty()) {
githubViewModel.searchRepos(query)
}
true
} else {
false
}
}
btnSearch.setOnClickListener {
val query = etSearchQuery.text.toString()
if (query.isNotEmpty()) {
githubViewModel.searchRepos(query)
}
}
}
lifecycleScope.launch {
githubViewModel.repoResult.collectLatest { pagingData ->
repoListAdapter.submitData(pagingData)
}
}
repoListAdapter.addLoadStateListener { loadState ->
val errorState = when {
loadState.prepend is LoadState.Error -> loadState.prepend as LoadState.Error
loadState.append is LoadState.Error -> loadState.append as LoadState.Error
loadState.refresh is LoadState.Error -> loadState.refresh as LoadState.Error
else -> null
}
errorState?.let {
Timber.e("## 에러 : ${it.error}")
Toast.makeText(this, "\uD83D\uDE28 에러 : ${it.error}", Toast.LENGTH_LONG).show()
}
}
}
}
LiveData.observe()를 지우고 lifecycleScope.launch {} 안에서 뷰모델의 repoResult를 통해 collectLatest {}를 사용하고 있다.
이것은 Flow에서 수집된 새 값이 있을 때마다 블록 안의 코드를 실행하며 이전의 데이터 수집이 완료되지 않았어도 새 값을 받으면 블록을 즉시 실행한다. 리사이클러뷰 어댑터에 바뀐 값을 전달해서 화면을 바꿔야 하기 때문에 이것을 사용하면 알맞게 페이징 데이터들을 처리할 수 있다. 아래는 collectLatest의 문서다.
제공된 작업으로 지정된 Flow를 수집하는 터미널 Flow 연산자다. collect와의 중요한 차이점은 원래 Flow가 새 값을 내보내면 이전 값에 대한 작업 블록이 취소된다는 것이다
이제 실행하면 LiveData를 사용했을 때와 동일하게 작동할 것이다.
'Android' 카테고리의 다른 글
[Android] Espresso Web이란? 웹뷰 UI 테스트 예제 (0) | 2023.04.15 |
---|---|
[Android] Mocking이란? MockK vs Mockito (0) | 2023.04.14 |
[Android] 페이징 라이브러리, Hilt, LiveData로 Github API 사용하기 (0) | 2023.04.09 |
[Android] withContext란? (0) | 2023.04.08 |
[Android] CameraX 코드랩 뜯어보기 - 3 - (0) | 2023.03.27 |