[Android, Kotlin] Room DB에서 Flow를 사용하여 DB 변경 observing
Android

[Android, Kotlin] Room DB에서 Flow를 사용하여 DB 변경 observing

조건 :

찜한 글 목록에서 글 클릭 -> 클릭한 글의 detail 화면에서 찜 해제 시 DB에서 찜한 글 목록이 바로 업데이트 됨 ->

다시 찜한 글 목록으로 돌아왔을 때 업데이트 된 찜한 글 목록이 나오도록 해야 함.

 

문제 : 다시 찜한 글 목록으로 돌아왔을 때 찜한 글이 update되지 않음.

 

띠용

 

// FavoritePostViewModel.kt
val posts: StateFlow<Result<List<Post>>> =
    favoriteRepository.getAllPosts()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = Result.Uninitialized
        )

ViewModel에서는 Repository에서 getAllPosts() 메소드를 통해 Room DB에서 찜한 모든 post를 가져오는 flow를 받아 StateFlow로 변환해 observe한다. 데이터 바인딩을 통해 해당 StateFlow를 observe하여 찜한 글 목록이 업데이트된다.

 

// FavoriteRepository.kt
fun getAllPosts() = flow {
        emit(Result.Loading)
        val posts = postDao.getAllPosts()
        if (posts.isEmpty()) {
        	emit(Result.Empty)
        } else {
        	emit(Result.Success(posts))
        }
    }.catch { e ->
        emit(Result.Error(e))
    }

Repository에서는 Room DB에 저장되어 있는 모든 찜한 글 목록을 가져와 그 결과를 emit하는 flow를 return 한다.

 

// PostDao.kt
@Query("SELECT * FROM Post")
suspend fun getAllPosts(): List<Post>

Dao에서는 Room DB에 저장되어 있는 모든 찜한 글 목록을 Query문을 통해 가져온다.

 

문제는 Dao에서의 getAllPosts() 메소드이다. 해당 메소드는 List<Post>를 반환하므로, Repository의 getAllPosts() 메소드가 수행 될 때에만 Room DB에서 값을 가져온다.

 

그러나, ViewModel에서는 StateFlow 형태로 Repository의 getAllPosts() 메소드의 return flow를 collect하는데,

Repository의 getAllPosts() flow는 Dao에서 flow가 아닌 List<Post>를 return하므로 찜한 글 목록을 실시간으로 emit하지 않고 Repository의 getAllPosts() 메소드가 수행될 때만 Result.Success(posts)가 emit된다.

즉, ViewModel이 파괴되고 재생성 되기 전까지는 찜한 글이 업데이트 되지 않는다.

 

 

 

해결 : 

이를 해결하기 위해 Room에서는 Flow 타입의 return을 제공한다.

 

Room의 Dao를 Flow 타입의 return 형태로 바꾼다.

 

//PostDao.kt (변경)
@Query("SELECT * FROM Post")
fun getAllPosts(): Flow<List<Post>>

 

Flow 타입으로 바뀌면서 suspend 키워드가 빠지게 되었다.

flow의 경우에는 flow가 반환되는 즉시 flow 내부의 동작을 수행하는 것이 아니라 collect와 같은 메소드가 있어야 내부 함수가 실행되므로 suspend 키워드가 쓰이지 않는 것이다.

 

위와 같이 return 값을 Flow<List<Post>>로 하게 되면, "SELECT * FROM Post"의 결과가 변경 될 때마다 변경된 결과가 Flow로 반환 된다. 실시간으로 Room DB의 업데이트가 반영되는 가장 중요한 키포인트이다.

 

Flow로 변경된 Dao의 return 값에 맞춰, Repository도 실시간으로 업데이트를 반영할 수 있도록 바꾸어본다.

 

 

// FavoriteRepository.kt (변경)
fun getAllPosts() = flow {
    emit(Result.Loading)
    // collect를 통해 Flow 반환형을 실시간으로 수집하여 emit한다.
    postDao.getAllPosts().collect {
            Timber.tag("likedPost").d("Liked post change")
            if (it.isEmpty()) {
                emit(Result.Empty)
            } else {
                emit(Result.Success(it))
            }
        }
}.catch { e ->
    emit(Result.Error(e))
}

 

Dao.getAllPosts() 메소드에서 반환한 Flow를 collect하여 값이 바뀔 때마다 collect block 내부에서 그 결과를 emit한다.

이를 통해 View의 변화에 상관 없이 Room DB의 값이 바뀔 때마다 바뀐 값이 emit되어

ViewModel의 StateFlow에 전달되고, observe하는 DataBinding에 의해 찜한 글 목록이 업데이트 된다.

 

 

 

바뀐 코드에 의해 정상 동작

 

 

 

 

전체 소스코드  : https://github.com/HunSeongPark/post-sample-hilt-jetpack

 

GitHub - HunSeongPark/post-sample-hilt-jetpack: 기존 post-sample Repository 공부한 기술들로 다시 만들어보기 (MVV

기존 post-sample Repository 공부한 기술들로 다시 만들어보기 (MVVM, Hilt, Coroutine/Flow, DataBinding, Retrofit, Moshi, Jetpack) - GitHub - HunSeongPark/post-sample-hilt-jetpack: 기존 post-sample Repository 공부한 기술들로 ...

github.com