์ฝ๋ฃจํด์์ ํ๋ก์ฐ๋ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ด๋ฉฐ ์ฝ๋ฃจํด ์์์ ๋ฆฌ์กํฐ๋ธ ํ๋ก๊ทธ๋๋ฐ์ ์ง์ ํ๊ธฐ ์ํ ๊ตฌ์ฑ ์์๋ค. ํ๋ก์ฐ๋ suspend function์ ์ฌ์ฉํ์ฌ ๊ฐ์ ๋น๋๊ธฐ์ ์ผ๋ก ์์ฑํ๊ณ ์ฌ์ฉํ๋ค. ์ฆ ๊ธฐ๋ณธ ์ค๋ ๋๋ฅผ ์ฐจ๋จํ์ง ์๊ณ ๋คํธ์ํฌ ์์ฒญ์ด ๊ฐ๋ฅํ๋ค.
Producer
์์ฐ์๋ DataSource๋ก๋ถํฐ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ ์คํธ๋ฆผ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ๋ค. flow ๋ธ๋ก ์์์ emit ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๋ด๋ณด๋ธ๋ค.
class KakaoRemoteDataSource @Inject constructor(
private val kakaoService: KakaoService
) : KakaoDataSource {
override fun getKeywordPlace(
keyword: String
): Flow<PlaceKeywordResult> = flow{
emit(kakaoService.getKeywordPlace(keyword))
}
}
์๋ ์์์๋ ๋ฐ์ดํฐ ์์ค๊ฐ ๊ณ ์ ๋ ๊ฐ๊ฒฉ์ผ๋ก ์ต์ ๋ด์ค๋ฅผ ๊ฐ์ ธ ์จ๋ค.
class NewsRemoteDataSource(
ย ย private val newsApi: NewsApi,
ย ย private val refreshIntervalMs: Long = 5000
) {
ย ย val latestNews: Flow<List<ArticleHeadline>> = flow {
ย ย ย ย while(true) {
ย ย ย ย ย ย val latestNews = newsApi.fetchLatestNews()
ย ย ย ย ย ย emit(latestNews) // Emits the result of the request to the flow
ย ย ย ย ย ย delay(refreshIntervalMs) // Suspends the coroutine for some time
ย ย ย ย }
ย ย }
}
Intermediary
์ค๊ฐ์๋ ์คํธ๋ฆผ์ ๋ด๋ณด๋ด๋ ๊ฐ๊ฐ์ ๊ฐ์ด๋ ์คํธ๋ฆผ ์์ฒด๋ฅผ ์์ ํ ์ ์๋ค. ์๋ฅผ ๋ค์ด ์๋ฒ๋ก ๋ถํฐ ๋ฐ์ ๋ฐ์ดํฐ๋ ๋ฐ์ดํฐ์ ์๋ฃํ์ด view์์ ํ์ํ๋๋ก ๋ฐ๊พธ๋ ์์ ์ด ์ด์ ํด๋นํ๋ค. ์ค๊ฐ ์ฐ์ฐ์์๋ **map(๋ฐ์ดํฐ ๋ณํ), filter(๋ฐ์ดํฐ ํํฐ๋ง), onEach(๋ชจ๋ ๋ฐ์ดํฐ ๋ง๋ค ์ฐ์ฐ ์ํ)**์ด ์๋ค.
class KakaoRepositoryImpl @Inject constructor(
private val kakaoRemoteDataSource: KakaoRemoteDataSource,
): KakaoRepository {
override fun getKeywordPlace(
keyword: String
): Flow<List<Place>> = kakaoRemoteDataSource.getKeywordPlace(keyword).map{
it.documents.map{p->Place(p.place_name, p.category_name, p.x, p.y)}
}
}
Consumer
์๋น์๋ collect๋ฅผ ์ด์ฉํ์ฌ ์คํธ๋ฆผ์ ๊ฐ์ ์ฌ์ฉํ๋ค. collect๋ suspend function์ด๋ฏ๋ก ์ฝ๋ฃจํด ๋ด์์ ์คํ๋์ด์ผ ํ๋ค.
class LatestNewsViewModel(
ย ย private val newsRepository: NewsRepository
) : ViewModel() {
ย ย init {
ย ย ย ย viewModelScope.launch {
ย ย ย ย ย ย // Trigger the flow and consume its elements using collect
ย ย ย ย ย ย newsRepository.favoriteLatestNews.collect { favoriteNews ->
ย ย ย ย ย ย ย ย // Update View with the latest favorite news
ย ย ย ย ย ย }
ย ย ย ย }
ย ย }
}
catch ๋ธ๋ก์ ์ฌ์ฉํ์ฌ ์์ธ๋ฅผ ์ฒ๋ฆฌํ ์ ์๋ค.
class LatestNewsViewModel(
ย ย private val newsRepository: NewsRepository
) : ViewModel() {
ย ย init {
ย ย ย ย viewModelScope.launch {
ย ย ย ย ย ย newsRepository.favoriteLatestNews
ย ย ย ย ย ย ย ย // Intermediate catch operator. If an exception is thrown,
ย ย ย ย ย ย ย ย // catch and update the UI
ย ย ย ย ย ย ย ย .catch { exception -> notifyError(exception) }
ย ย ย ย ย ย ย ย .collect { favoriteNews ->
ย ย ย ย ย ย ย ย ย ย // Update View with the latest favorite news
ย ย ย ย ย ย ย ย }
ย ย ย ย }
ย ย }
}
withContext์ ๋น์ทํ๊ฒ ์ํํ๋ context๋ฅผ ๋ฐ๊ฟ ์ ์๋ค. ๊ทธ๋ฌ๋ ์ฝ๋ฃจํด์์ emit์ ํ๋ context์ collectํ๋ context๋ ๊ฐ์์ผ ํ๋ค. ์ด๋ฐ ๊ฒฝ์ฐ flowOn์ ์ฌ์ฉํ์ฌ emitํ๋ ๋ถ๋ถ์ context๋ฅผ ๋ฐ๊ฟ ์ ์๋ค. flowOn ์ฐ์ฐ์๋ flowOn ์์ ๋ถ๋ถ(upstream)๋ง ์ํฅ์ ๋ฐ๋๋ค.
class NewsRemoteDataSource(
ย ย ...,
ย ย private val ioDispatcher: CoroutineDispatcher
) {
ย ย val latestNews: Flow<List<ArticleHeadline>> = flow {
ย ย ย ย // Executes on the IO dispatcher
ย ย ย ย ...
ย ย }
ย ย ย ย .flowOn(ioDispatcher)
}
๋ฐ์ดํฐ ์์ค ๋ ์ด์ด๊ฐ I/O ์์ ์ ์ํํ๋ฏ๋ก I/O ์์ ์ ์ต์ ํ๋ ๋์คํจ์ณ๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค.
ํ์ง๋ง Flow๋ ๋ฐ์ดํฐ์ ํ๋ฆ์ ๋ฐ์์ํค๊ธฐ๋ง ํ ๋ฟ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ์ง ์๋๋ค. ์ฆ ํ๋ฉด์ด ์ฌ๊ตฌ์ฑ๋ ๋๋ง๋ค ์๋ฒ๋ ๋ก์ปฌ DB๋ก๋ถํฐ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๊ฑฐ๋ ๋ฐ์ดํฐ ๊ฐ์ด collect ๋ ๋๋ง๋ค ViewModel์ ์ ์ฅํด๋๊ณ ์ฌ์ฉํด์ผํ๋ค. ์ ์๋ ๋งค์ฐ ๋นํจ์จ์ ์ธ ๋ฐฉ๋ฒ์ด๋ฏ๋ก ์ฌ์ฉํ์ง ์๋๋ค.
๊ธฐ์กด์๋ LiveData๋ฅผ ์ฌ์ฉํ์ฌ ์๋ฒ๋ก ๋ถํฐ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์๋ค. LiveData๋ ์ต์ ๋ฒ ํจํด์ ํ์ฉํ์ฌ ๊ตฌํ๋์๊ณ ์๋ช ์ฃผ๊ธฐ์ ๋ณํ๋ฅผ ์์ฒด์ ์ผ๋ก ์ธ์ํ๋ค๋ ์ฅ์ ์ด ์๋ค.
ํ์ง๋ง Clean Architecture๋ฅผ ๋์ ํ๋ฉด์ Domain Layer์์๋ ๊ทธ๋๋ก LiveData๋ฅผ ์ ์ฉํ๋ ๊ฒฝ์ฐ๊ฐ ์๋๋ฐ ์ด๋ ๋ง์ง ์๋ค. ๋๋ฉ์ธ ๊ณ์ธต์ ์๋๋ก์ด๋์ ์์กด์ฑ์ ๊ฐ์ง์ง ์๋ ์์ ์ฝํ๋ฆฐ ์ฝ๋๋ก ์์ฑ๋์ด์ผ ํ๋ค. ๊ทธ๋ฌ๋ LiveData๋ UI์ ๋งค์ฐ ๋ฐ์ ํ๊ฒ ์ฐ๊ฒฐ๋์ด ์๊ณ ์๋๋ก์ด๋ ํ๋ซํผ์ ์ํด์๊ธฐ ๋๋ฌธ์ ๋๋ฉ์ธ ๋ ์ด์ด์์ ์ฌ์ฉํ๊ธฐ์ ์ ํฉํ์ง ์๋ค.