이번 영상에서는 FavoriteFragment에 사용한 Room의 Livedata 응답을 Flow로 변경하는 법에 대해 알아보도록 하겠습니다.
우선은 Room의 query 응답을 LiveData에서 Flow로 교체해줍니다.
@Dao
interface BookSearchDao {
...
@Query("SELECT * FROM books")
- fun getFavoriteBooks(): LiveData<List<Book>>
+ fun getFavoriteBooks(): Flow<List<Book>>
}
다음은 리포지토리의 반환 타입을 변경합니다.
interface BookSearchRepository {
...
- fun getFavoriteBooks(): LiveData<List<Book>>
+ fun getFavoriteBooks(): Flow<List<Book>>
}
class BookSearchRepositoryImpl(
private val db: BookSearchDatabase
) : BookSearchRepository {
...
- override fun getFavoriteBooks(): LiveData<List<Book>> = db.bookSearchDao().getFavoriteBooks()
+ override fun getFavoriteBooks(): Flow<List<Book>> = db.bookSearchDao().getFavoriteBooks()
}
ViewModel의 반환 타입도 변경하구요.
class BookSearchViewModel(
private val bookSearchRepository: BookSearchRepository,
) : ViewModel() {
...
- val favoriteBooks: LiveData<List<Book>> = bookSearchRepository.getFavoriteBooks()
+ val favoriteBooks: Flow<List<Book>> = bookSearchRepository.getFavoriteBooks()
}
이렇게 응답을 변경함으로써 도메인과 UI Layer에서 LiveData에 관련된 import가 모두 제거되었습니다.
다음은 쿼리 결과를 UI에 표시하겠습니다. observe를 사용하던 LiveData와 달리 Flow는 코루틴 안에서 collect 나 collectLatest를 써서 데이터를 구독해야 합니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
- bookSearchViewModel.favoriteBooks.observe(viewLifecycleOwner, Observer {
- bookSearchAdapter.submitList(it)
- })
+ lifecycleScope.launch {
+ bookSearchViewModel.favoriteBooks.collectLatest {
+ bookSearchAdapter.submitList(it)
+ }
+ }
}
실행을 시켜보면 기존의 데이터가 정상적으로 표시되는 것을 확인할 수 있습니다.
이제 favoriteBooks를 StateFlow로 변환해 Flow의 동작을 FavoriteFragment의 라이프사이클과 동기화시키도록 하겠습니다. 우선은 stateIn을 써서 타입을 변경해 줍니다. 스코프는 viewModelScope, 구독을 시작하는 시점은 SharingStarted.WhileSubscribed, 그리고 List<Book>의 초기값을 전달해 주면 됩니다.
class BookSearchViewModel(
private val bookSearchRepository: BookSearchRepository,
) : ViewModel() {
...
- val favoriteBooks: Flow<List<Book>> = bookSearchRepository.getFavoriteBooks()
+ val favoriteBooks: StateFlow<List<Book>> = bookSearchRepository.getFavoriteBooks()
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), listOf())
}
그리고나서 다음과 같이 viewLifecycleOwner와 repeatOnLifecycle을 조합함으로써 프래그먼트의 라이프사이클에 맞춰 구독과 정지를 수행하는 코루틴을 구축하고 그 안에서 StateFlow를 구독하면 됩니다.
생명주기에 맞춰 flow를 구독하는 API로는 launchWhenStarted도 있는데 launchWhenStarted는 처음에 한 번만 실행되고 그 다음 생명주기가 돌아와도 실행되지 않으나, repeatOnLifecycle은 지정한 생명주기가 돌아올 때마다 코루틴 블록을 실행하고 생명주기가 끝날 때 코루틴을 취소하는 특징이 있습니다. 이들 API는 flow가 생명주기를 인식하지 못하는 부분을 보완하기 위해서 만들어졌는데 구글에서는 launchWhenStarted 보다 repeatOnLifecycle을 사용하도록 권고하고 있습니다. 더 구체적인 배경과 사용예에 대해서는 repeatOnLifecycle API design story 를 참고하시면 좋을 것 같습니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
- lifecycleScope.launch {
- bookSearchViewModel.favoriteBooks.collectLatest {
- bookSearchAdapter.submitList(it)
- }
- }
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ bookSearchViewModel.favoriteBooks.collectLatest {
+ bookSearchAdapter.submitList(it)
+ }
+ }
+ }
}
StateFlow를 구독하는 코루틴 블록이 너무 길다고 느껴진다면 확장함수를 만드는 것도 방법이 될 수 있습니다. 다음과 같이 flow와 서스펜드 함수를 파라미터로 받는 확장함수를 util > Extensions.kt로 만들어 줍니다.
fun <T> FavoriteFragment.collectLatestStateFlow(flow: Flow<T>, collect: suspend (T) -> Unit) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
flow.collectLatest(collect)
}
}
}
이 코드에서 collectLatestStateFlow 함수는 파라미터로 flow와 collect를 받습니다. flow는 UI에 표시할 Flow 형 데이터로 bookSearchViewModel.favoriteBooks이고, collect는 그 데이터를 표시하는데 쓰일 함수로 bookSearchAdapter.submitList가 됩니다.
collectLatestStateFlow 함수의 파라미터로 collect: suspend (T) -> Unit라는 람다식을 사용하면, 파라미터가 없는 suspend 함수를 받아서 작업을 수행하고 아무것도 반환하지 않는 함수를 collect라는 이름의 파라미터로 전달하겠다는 뜻이 됩니다. bookSearchAdapter.submitList가 그러한 collect 역할을 하는 함수로 전달되는 것이지요.
작성한 확장함수는 다음과 같이 사용하면 됩니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
+ collectLatestStateFlow(bookSearchViewModel.favoriteBooks) {
+ bookSearchAdapter.submitList(it)
+ }
}