Android에서 flow를 통한 실시간 데이터 갱신에 대한 정리



개인 광고 영역

작성한 GitHubUserSearch 소스를 기반으로 flow를 통해 실시간 데이터 갱신하는 방법에 대해 정리해 본다.

이 방식을 사용하면 데이터를 다시 부르기 위한 형태가 필요치 않고, 실시간 데이터를 지속적으로 받을 수 있는 장점이 생긴다.

여기에서 설명하는 코드는 매뉴얼 인젝션을 사용한 코드이다.


이 글에서 알아볼 내용

  • coroutines + flow를 통한 실시간 데이터 갱신


GitHub 샘플 코드 소개

링크 : GitHub User search

이 코드는 2개의 탭으로 구성되어 있고, 3가지 필터를 옵션으로 제공한다.

기본 내용은 아래와 같다.

  • 검색 화면
    • 메인 화면에서는 검색을 통해 사용자를 검색
    • 좋아요를 누르면 좋아요 페이지와 동기화
    • 싫어요를 누르면 검색 화면과 좋아요 페이지에서 실시간 갱신되어야 한다.
  • 좋아요 페이지
    • 좋아요 한 결과물 모두가 노출
    • 좋아요를 빼면 목록이 갱신된다.


어떻게 구성할까?

아키텍처는 MVVM을 기반으로 작업했다. 이전 코드에서는 RxJava를 사용하였는데, 이 부분도 코드에서 확인 할 수 있다.

첫 번째 글에서는 메뉴얼 인젝션을 사용했고, 모듈화에 대해서는 설명하지 않는다.


실시간 데이터 업데이트

Room과 Flow를 활용하면 실시간 데이터 갱신은 매우 쉽다.

Room 데이터베이서 flow를 적용한다. insert, delete, updateselect에서 걸어둔 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가지가 있다.

  1. 루프를 처리하는 과정에 dao에 해당하는 ID 값을 쿼리하고, 일치하면 true 일치하지 않으면 false를 리턴한다.
  2. 전체 데이터를 받아온다음 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()에서도 실시간 데이터를 받아올 수 있다.

플로우 차트로 그리면 아래와 같고,

image_01

데이터 흐름으로 보면 아래와 같다.

image_02

결국 위 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를 통해 아래와 같이 걸어둔다. likesort에 따라 아랫부분의 코드 흐름이 제어되어 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로 작성한 코드에 반영해두었다.

링크 : RxJava를 사용한 샘플

다음 글에서 패키지 구조에 대해서 설명하겠다.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments