Android Multi Module Clean Architecture with Hilt, Ktor Client (3) - Caching

이태훈·2022년 1월 12일
1

안녕하세요, 이 시리즈로 뵙는 거는 오랜만이네요.

사실, 저번 편에서 마무리할려고 했는데, 문득 데이터 캐싱을 주제로 시리즈를 더 이어갈 수 있을 것 같아서 더 작성하게 됐습니다..

이번 시리즈의 주제는 위에서 말씀드렸듯이 데이터 캐싱, 데이터베이스 데이터 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

profile
https://www.linkedin.com/in/%ED%83%9C%ED%9B%88-%EC%9D%B4-7b9563237

0개의 댓글