Android에서 flow를 통한 실시간 데이터 갱신에 대한 정리
개인 광고 영역
작성한 GitHubUserSearch 소스를 기반으로 flow를 통해 실시간 데이터 갱신하는 방법에 대해 정리해 본다.
이 방식을 사용하면 데이터를 다시 부르기 위한 형태가 필요치 않고, 실시간 데이터를 지속적으로 받을 수 있는 장점이 생긴다.
여기에서 설명하는 코드는 매뉴얼 인젝션을 사용한 코드이다.
이 글에서 알아볼 내용
- coroutines + flow를 통한 실시간 데이터 갱신
GitHub 샘플 코드 소개
이 코드는 2개의 탭으로 구성되어 있고, 3가지 필터를 옵션으로 제공한다.
기본 내용은 아래와 같다.
- 검색 화면
- 메인 화면에서는 검색을 통해 사용자를 검색
- 좋아요를 누르면 좋아요 페이지와 동기화
- 싫어요를 누르면 검색 화면과 좋아요 페이지에서 실시간 갱신되어야 한다.
- 좋아요 페이지
- 좋아요 한 결과물 모두가 노출
- 좋아요를 빼면 목록이 갱신된다.
어떻게 구성할까?
아키텍처는 MVVM을 기반으로 작업했다. 이전 코드에서는 RxJava를 사용하였는데, 이 부분도 코드에서 확인 할 수 있다.
첫 번째 글에서는 메뉴얼 인젝션을 사용했고, 모듈화에 대해서는 설명하지 않는다.
실시간 데이터 업데이트
Room과 Flow를 활용하면 실시간 데이터 갱신은 매우 쉽다.
Room 데이터베이서 flow를 적용한다. insert
, delete
, update
시 select
에서 걸어둔 Flow를 통해 자동으로 최신 데이터를 흘려보내준다.
@Dao
interface GitHubUserDao {
@Query("SELECT * FROM gitHubUser ORDER BY score DESC")
fun flowLiked(): Flow<List<GitHubUser>>
}
API를 통해 불러온 데이터와 로컬 데이터 합치는 과정은?
우선 API를 통해 불러오는 데이터는 단방향성을 가지는데, 1번 호출하면 이후에 재호출할 필요는 없다.
그럼 이 과정에서 flow를 왜 사용할까?
- flow를 걸어두면 뒤쪽 데이터 가공에 용이하다.
필자는 아래와 같이 데이터를 불러왔는데, map
에서 searchUser를 통해 받아온 리스트로 치환 시켜줬다.
flow {
if (cacheSearchKeyword == searchKeyword) {
page++
} else {
clear()
}
emit(true)
}
.map { // < -- 이 부분
gitHubApi.searchUser(
searchKeyword = searchKeyword,
page = page,
perPage = perPage,
)
}
그럼 like 한 데이터랑은 언제 합칠까?
방법은 2가지가 있다.
- 루프를 처리하는 과정에
dao
에 해당하는 ID 값을 쿼리하고, 일치하면true
일치하지 않으면false
를 리턴한다. - 전체 데이터를 받아온다음 find 형태로 찾아서 처리한다.
필자는 2 번 방법을 선택해 코드 작업을 하였다.
1 번 방법은 정말 1회 성 형태로 사용하는 경우에 해당한다. 위에서 적었지만 실시간 데이터가 반영되어야 한다.
1 번 방법을 선택했다면 다음과 같은 코드를 추가로 작성해야 한다.
- 좋아요 아이템을 list에 추가
- 선택 1. 좋아요 한 아이템과 기존에 불러온 리스트를 캐싱 해두고 이를 동기화한다.
- 이 방법은 좋아요 한 시점을 명확히 알기 때문에 데이터를 명시적으로 한 번 더 불러야 한다.
- 선택 2. 이미 리스트에 그려져있으니 이 아이템을 화면 갱신하면서 find 하여 찾는다.
- 이 방법을 선택한다면 상당히 비효율적이다.
다시 돌아가서 2 번 방법을 선택한 코드를 보자.
.flatMapLatest { items -> // < -- 여기서 flatMapLatest를 한 번 더 호출하고, flowLiked 데이터를 처리한다.
gitHubUserDao.flowLiked()
.map {
it to items
}
}
.map { (likedList, items) -> // < -- 두 값을 머지하는 Entity를 만든다.
items.map { item ->
GitHubUserEntity(
id = item.id,
login = item.login,
avatarUrl = item.avatarUrl,
score = item.score,
isLike = likedList.firstOrNull { likedItem -> likedItem.id == item.id } != null,
)
}
}
이렇게 작성하면
이렇게 작성하면 searchUser
를 통해 데이터를 받아온 다음, dao.flowLiked()
에서도 실시간 데이터를 받아올 수 있다.
플로우 차트로 그리면 아래와 같고,
데이터 흐름으로 보면 아래와 같다.
결국 위 flatMapLatest
를 통해 flow를 묶어두면 데이터 갱신 위치와 범위에 따라 자동으로 반응하는 것을 볼 수 있다.
지금의 코드에서는 like
를 누르면 dao
에 묶어둔 gitHubUserDao.flowLiked()
를 통해 최신의 데이터가 업데이트되어 흘러오고, 이를 Entity로 교체한다.
그럼 별다른 코드 설계 없이도 자동으로 UI까지 실시간 데이터 갱신이 가능해진다.
Sort를 추가한다면
sort도 flatMapLatest
를 통해 동일한 형태를 만들 수 있다.
이 코드가 반응되어야 할 부분은 좋아요 처리 이후가 된다.
.flatMapLatest { data ->
sortList
.map { data }
}
.map { (likedList, items) ->
items.map { item ->
GitHubUserEntity(
id = item.id,
login = item.login,
avatarUrl = item.avatarUrl,
score = item.score,
isLike = likedList.firstOrNull { likedItem -> likedItem.id == item.id } != null,
)
}
}
.map { newList ->
newList.sort(sortList.value)
}
ViewModel에서는
ViewModel에서는 loadData()
flow를 통해 아래와 같이 걸어둔다. like
와 sort
에 따라 아랫부분의 코드 흐름이 제어되어 UI가 갱신된다.
.flatMapLatest { searchKeyword ->
gitHubSearchRepository.flowLoadData(
searchKeyword = searchKeyword,
perPage = 30,
)
}
.map {
it.convert()
}
.onEach {
loadData.value = false
_mainListUiState.value = it
_showProgress.value = false
}
마무리
이 글에서는 flow를 통한 데이터 갱신에 대해서 이야기했다. 중간에 데이터를 갱신하도록 처리할 필요도 없고, flatMapLatest
와 다른 flow
를 잘 합쳐만 두더라도, 알아서 반응함이 중요하다.
코틀린 flow를 사용한다면 이러한 부분을 잘 활용하여 화면 갱신이 매우 쉬워진다.
- 리스트를 불러온다.
- 좋아요 리스트랑 merge 한다.
- 정렬 방식에 따라 데이터를 정렬한다.
액션부터 설명하는 순서대로 본다면
- 사용자 액션을 통해 like/unlike를 처리한다.
- Room database에
insert
/delete
를 처리한다. - flow를 통해 room database를 실시간으로 갱신하는데, flow의
flatMapLatest
에서 자동으로 반응한다. entity
로 머지하고, viewModel에서 구독해둔 Ui를 갱신한다.
결국 스트림 흐름은 1개이고, 이를 통해 실시간 갱신을 한다는 것이다.
또 다른 방식도 가능한데, 단순히 MutableStateFlow
를 하나 만들어두고, 데이터만 갱신해 주는 방식도 가능한데, 이런 방법은 RxJava로 작성한 코드에 반영해두었다.
다음 글에서 패키지 구조에 대해서 설명하겠다.
Comments