# Data Layer

> 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: `thetruong1099/data-layer` (Agent Skill)
- Install (CLI): `npx skillmds@latest add thetruong1099/data-layer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/thetruong1099/data-layer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: thetruong1099 (https://skillmd.com/u/thetruong1099)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/thetruong1099/data-layer

---


# 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

```kotlin
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

```kotlin
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

```kotlin
// 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

```kotlin
// 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

```kotlin
// 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

```kotlin
// 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

```kotlin
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.

