agentsclimarketplace

Data layer

Skill thetruong1099/android-mvi-base-code/.claude/skills/data-layer

Install
npx -y skills add thetruong1099/android-mvi-base-code --skill data-layer

Assembled 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/)

StrategyInputOutputDispatcher
FirebaseDataSourceStrategyTask<T>Flow<DataState<T>>Dispatchers.IO
RetrofitDataSourceStrategyResponse<BaseDto<T>>Flow<DataState<T>>Dispatchers.IO
PagingDataSourceStrategyPagingSourceFlow<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.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.