Data layer
Skill thetruong1099/android-mvi-base-code/.claude/skills/data-layer
npx -y skills add thetruong1099/android-mvi-base-code --skill data-layerAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Data layer architecture patterns for this project. Use when implementing repositories (interface in domain/repository, impl in data/remote-data), data sources extending BaseDataSource with fetchData/fetchRestData/fetchPagingData, mappers using BaseMapper or BidirectionalMapper, Hilt DI modules for repositories/data sources, or custom PagingSource classes.
SKILL.md
7.0 KB, as published. Nobody here has run it
Data Layer
Architecture
data/
core/ -> BaseDataSource, BaseMapper, ExceptionMapper, DTO base classes
local-data/ -> Room database, DataStore, DAOs
remote-data/ -> Retrofit services, Firebase sources, Repository implementations
BaseDataSource + Strategy Pattern
open class BaseDataSource {
// Firebase Task<T> -> Flow<DataState<T>>
protected fun <T> fetchData(call: suspend () -> Task<T>): Flow<DataState<T>>
// Retrofit Response<BaseDto<T>> -> Flow<DataState<T>>
protected fun <T> fetchRestData(call: suspend () -> Response<BaseDto<T>>): Flow<DataState<T>>
// Paging3 -> Flow<PagingData<T>>
protected fun <Key, Value> fetchPagingData(
config: PagingConfig,
remoteMediator: RemoteMediator<Key, Value>? = null,
pagingSourceFactory: () -> PagingSource<Key, Value>,
): Flow<PagingData<Value>>
// Simple sync wrapper -> Flow<DataState<T>>
protected fun <T> requestData(call: () -> T): Flow<DataState<T>>
}
Strategy Classes (object singletons in data/core/.../strategy/)
| Strategy | Input | Output | Dispatcher |
|---|---|---|---|
FirebaseDataSourceStrategy | Task<T> | Flow<DataState<T>> | Dispatchers.IO |
RetrofitDataSourceStrategy | Response<BaseDto<T>> | Flow<DataState<T>> | Dispatchers.IO |
PagingDataSourceStrategy | PagingSource | Flow<PagingData<T>> | Dispatchers.IO |
Creating a DataSource
class ItemRemoteDataSource @Inject constructor(
private val apiService: ItemApiService,
private val itemMapper: ItemMapper,
) : BaseDataSource() {
// REST API call
fun getItemById(id: String): Flow<DataState<Item>> =
fetchRestData { apiService.getItem(id) }
.map { state ->
when (state) {
is DataState.Success -> DataState.Success(itemMapper.toDomain(state.data))
is DataState.Error -> state
is DataState.Loading -> state
}
}
// Firebase call
fun getItemFromFirebase(id: String): Flow<DataState<ItemDto>> =
fetchData { firestore.collection("items").document(id).get() }
// Paging call
fun getItemsPaged(): Flow<PagingData<Item>> =
fetchPagingData(
config = PagingConfig(pageSize = 20, enablePlaceholders = false),
pagingSourceFactory = { ItemPagingSource(apiService, itemMapper) },
)
}
Mapper Pattern
// One-way: DTO -> Domain
interface BaseMapper<DTO, DOMAIN> {
fun toDomain(dto: DTO): DOMAIN
fun toDomainList(dtos: List<DTO>): List<DOMAIN> = dtos.map { toDomain(it) }
}
// Two-way: DTO <-> Domain
interface BidirectionalMapper<DTO, DOMAIN> : BaseMapper<DTO, DOMAIN> {
fun toDto(domain: DOMAIN): DTO
fun toDtoList(domains: List<DOMAIN>): List<DTO> = domains.map { toDto(it) }
}
// Auto-generated with Konvert KSP
@Konvert(fromClass = ItemDto::class, toClass = Item::class)
interface ItemMapper : BaseMapper<ItemDto, Item>
// Or manual
class ItemMapper @Inject constructor() : BaseMapper<ItemDto, Item> {
override fun toDomain(dto: ItemDto): Item = Item(id = dto.id, name = dto.name)
}
DTO Structure
// REST API wrapper - RetrofitDataSourceStrategy validates chain:
// HTTP success -> body not null -> success=true -> data not null
data class BaseDto<T>(val success: Boolean, val data: T?, val error: String?)
data class ApiErrorDto(val httpCode: Int?, val error: String?)
Repository Pattern
// Interface in domain/repository/
interface ItemRepository {
fun getItems(): Flow<PagingData<Item>>
fun getItemDetail(id: String): Flow<DataState<Item>>
fun searchItems(keyword: String): Flow<PagingData<Item>>
suspend fun saveItem(id: String): Flow<DataState<Any>>
}
// Implementation in data/remote-data/
class ItemRepositoryImpl @Inject constructor(
private val remoteDataSource: ItemRemoteDataSource,
private val localDataSource: ItemLocalDataSource,
) : ItemRepository {
override fun getItems(): Flow<PagingData<Item>> =
remoteDataSource.getItemsPaged()
override fun getItemDetail(id: String): Flow<DataState<Item>> =
remoteDataSource.getItemById(id)
}
Hilt DI Modules
// Repository Module (data/remote-data/di/)
@Module @InstallIn(SingletonComponent::class)
interface RepositoryModule {
@Binds fun bindItemRepository(impl: ItemRepositoryImpl): ItemRepository
}
// Network Module
@Module @InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides @Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.API_URL)
.addConverterFactory(MoshiConverterFactory.create())
.client(okHttpClient)
.build()
@Provides
fun provideItemApiService(retrofit: Retrofit): ItemApiService =
retrofit.create(ItemApiService::class.java)
}
// Database Module (data/local-data/di/)
@Module @InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides @Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
Room.databaseBuilder(context, AppDatabase::class.java, "app_db").build()
@Provides
fun provideItemDao(db: AppDatabase): ItemDao = db.itemDao()
}
Custom PagingSource
class ItemPagingSource(
private val apiService: ItemApiService,
private val mapper: ItemMapper,
) : PagingSource<Int, Item>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> {
val page = params.key ?: 1
return try {
val response = apiService.getItems(page = page, limit = params.loadSize)
val items = mapper.toDomainList(response.body()?.data ?: emptyList())
LoadResult.Page(
data = items,
prevKey = if (page == 1) null else page - 1,
nextKey = if (items.isEmpty()) null else page + 1,
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Item>): Int? =
state.anchorPosition?.let { anchor ->
state.closestPageToPosition(anchor)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchor)?.nextKey?.minus(1)
}
}
DataState Flow Pattern
All DataSource methods returning Flow<DataState<T>>:
Emit Loading -> Execute operation -> Emit Success/Error
Errors are automatically mapped by ExceptionMapper within each strategy class.