안녕하세요, 이 시리즈로 뵙는 거는 오랜만이네요.
사실, 저번 편에서 마무리할려고 했는데, 문득 데이터 캐싱을 주제로 시리즈를 더 이어갈 수 있을 것 같아서 더 작성하게 됐습니다..
이번 시리즈의 주제는 위에서 말씀드렸듯이 데이터 캐싱, 데이터베이스 데이터 observing으로 하고 포스팅하겠습니다. 잘 부탁드리겠습니다.
먼저, 이 프로젝트에서 사용할 로컬 데이터베이스는 Room을 사용하도록 하겠습니다.
우선 다음과 같이 dependency를 넣어주시는 것으로 시작하겠습니다.
implementation(androidx.room:room-runtime:${Versions.room})
implementation(androidx.room:room-ktx:${Versions.room})
annotationProcessor(androidx.room:room-compiler:${Versions.room})
kapt(androidx.room:room-compiler:${Versions.room})
포스팅 현재 룸 최신 버전 : 2.4.0
그럼 데이터베이스 스키마를 짜보도록 하겠습니다.
@Entity(tableName = "items")
data class ItemEntity(
val title: String,
val description: String,
@PrimaryKey(autoGenerate = true)
val id: Int = 0
)
요로코롬 작성했습니다. 저는 데이터 캐싱이 잘 되는 지 확인하기 위해 대충 UI에 표시하기 쉬울만한 클래스로 작성했습니다.
다음으로, Dao를 작성해줍니다.
@Dao
interface ItemDao {
@Query("SELECT * FROM items")
fun getItemList(): Flow<List<ItemEntity>>
@Insert
suspend fun insertItem(vararg item: ItemEntity)
@Insert
suspend fun insertItem(items: List<ItemEntity>)
}
여기서, 저는 Database 데이터를 observing 하기 위해 observe 하고 싶은 데이터에 Flow를 wrapping 해줬습니다. 이걸 빼먹으면 안 됩니다!
@Database(
entities = [ItemEntity::class],
version = 1
)
abstract class AppDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
companion object {
private const val databaseName = "item-db"
fun buildDatabase(context: Context): AppDatabase =
Room.databaseBuilder(context, AppDatabase::class.java, databaseName)
.fallbackToDestructiveMigration()
.build()
}
}
그 다음, 데이터베이스 정의해주구요.
@Singleton
class ItemService @Inject constructor(
@Named("item") private val httpClient: HttpClient
) {
suspend fun getItemList(): Result<List<ItemEntity>> {
return try {
// data 가져오는 코드 작성해야합니다. url이나 parameter를 작성해줘야 해요.
val response = httpClient.get<HttpResponse>()
if (response.status.isSuccess()) {
Result.Success(response.receive(), response.status.value)
} else {
Result.ApiError(response.status.description, response.status.value)
}
} catch (e: Exception) {
Result.NetworkError(e)
}
}
}
다음으로, remote datasource에서 데이터를 가져오는 코드를 작성해보았습니다. 저는 로컬 데이터베이스 데이터를 observing 잘 되는지 확인하기 위해 remote datasource에서 데이터 가져올 때 Exception이 나게 해서 로컬데이터베이스에서 값을 꺼내오도록 했습니다.
@Singleton
class ItemRepositoryImpl @Inject constructor(
private val itemService: ItemService,
private val itemDao: ItemDao
) : ItemRepository {
override fun getItems(): Flow<List<Item>> = flow {
when (val response = itemService.getItemList()) {
is Result.Success -> {
itemDao.insertItem(response.data)
emit(response.data.map { it.mapToDomainModel() })
}
is Result.ApiError, is Result.NetworkError -> {
emitAll(itemDao.getItemList().map { it.mapToDomainModel() })
}
else -> Unit
}
}
override suspend fun insertItem(item: Item) {
itemDao.insertItem(item.mapFromDomainModel())
}
private fun List<ItemEntity>.mapToDomainModel() =
map { it.mapToDomainModel() }
}
여기서, getItems() 함수만 보셔도 이 포스팅은 다 봤다 하셔도 무방합니다.
remoteDatasource에서 가져온 data의 result가 Error일 때 데이터베이스에서 꺼내온 flow를 emit 해줍니다.
emit이 아닌 emitAll을 해줘야 해요.
interface ItemRepository {
fun getItems(): Flow<List<Item>>
suspend fun insertItem(item: Item)
}
@Singleton
class GetItemListUseCase @Inject constructor(
private val itemRepository: ItemRepository
) {
operator fun invoke() = itemRepository.getItems()
}
@HiltViewModel
class MarketViewModel @Inject constructor(
getItemUseCase: GetItemListUseCase,
private val insertItemUseCase: InsertItemUseCase
) : ViewModel() {
val items = getItemUseCase().stateIn(
viewModelScope, SharingStarted.WhileSubscribed(5000), null
)
fun insertItem(item: Item) = viewModelScope.launch {
insertItemUseCase(item)
}
}
마지막으로 뷰모델에서 위와 같이 코드를 작성해주면 insertItem이 호출 되어 데이터베이스에 데이터가 업데이트될 때마다 별다른 호출을 해주지 않아도 items가 갱신됩니다 ~
Git Repository :
https://github.com/TaehoonLeee/multi-module-clean-architecture