Skip to main content
The data layer handles data persistence, network communication, and data source coordination. It uses Room for local storage, Retrofit for API calls, and implements the Repository pattern to provide a clean interface to domain and presentation layers.

Architecture overview

The data layer is organized into multiple modules:
  • data/local - Room database, DAOs, and database entities (DBOs)
  • data/remote - Retrofit services, API interfaces, and DTOs
  • data/repository - Repository implementations coordinating data sources
  • data/source - Data source interfaces defining contracts
  • data/session - Session and preferences management
This modular structure separates concerns and allows independent testing of each component.

Local data with Room

Database configuration

The Room database defines entities, version, and provides DAO access:
data/local/src/main/java/es/mobiledev/data/local/AppRoomDatabase.kt
@Database(
    entities = [
        ArticleDbo::class,
    ],
    version = DATABASE_VERSION,
)
abstract class AppRoomDatabase : RoomDatabase() {
    abstract fun articleDao(): ArticleDao

    companion object {
        fun buildDatabase(context: Context): AppRoomDatabase =
            Room
                .databaseBuilder(
                    context = context.applicationContext,
                    klass = AppRoomDatabase::class.java,
                    name = DATABASE_NAME,
                ).build()
    }
}

Database entities (DBOs)

Define data structures for local storage:
data/local/src/main/java/es/mobiledev/data/local/article/dbo/ArticleDbo.kt
@Entity(tableName = "articles")
data class ArticleDbo(
    @PrimaryKey val id: Long,
    val title: String,
    val imageUrl: String,
    val newsSite: String,
    val addedAt: String,
)
DBO (Database Object) naming convention distinguishes database entities from domain models (BO) and DTOs.

Data Access Objects (DAOs)

DAOs define database operations with type-safe queries:
data/local/src/main/java/es/mobiledev/data/local/article/dao/ArticleDao.kt
@Dao
interface ArticleDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun saveFavoriteArticle(article: ArticleDbo)

    @Delete
    suspend fun removeFavoriteArticle(article: ArticleDbo)

    @Query("SELECT * FROM articles")
    suspend fun getFavoriteArticles(): List<ArticleDbo>

    @Query("SELECT * FROM articles WHERE id = :articleId")
    suspend fun getFavoriteArticleById(articleId: Long): ArticleDbo?
}
Room supports complex queries including:
  • Joins across multiple tables
  • Observable queries with Flow
  • Transactions for atomic operations
  • Type converters for custom data types
  • Full-text search

Remote data with Retrofit

API service interface

Define REST API endpoints using Retrofit annotations:
data/remote/src/main/java/es/mobiledev/data/remote/article/ArticleWs.kt
interface ArticleWs {
    @GET("articles")
    suspend fun getArticles(
        @Query("limit") limit: Long,
        @Query("offset") offset: Long
    ): ArticleResponseDto

    @GET("articles/{id}/")
    suspend fun getArticleById(
        @Path("id") id: Long
    ): ArticleDto
}

Retrofit configuration

Configure HTTP client, JSON parsing, and interceptors with Hilt:
data/remote/src/main/java/es/mobiledev/data/remote/di/RemoteModule.kt
@Module
@InstallIn(SingletonComponent::class)
object RemoteModule {
    @Provides
    fun interceptorProvider(): Interceptor =
        HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        }

    @Provides
    fun okHttpClientProvider(
        interceptor: Interceptor
    ) = OkHttpClient
        .Builder()
        .addInterceptor(interceptor)
        .build()

    @Provides
    fun moshiProvider(): Moshi =
        Moshi
            .Builder()
            .add(KotlinJsonAdapterFactory())
            .build()

    @Provides
    fun retrofitProvider(
        okHttpClient: OkHttpClient
    ): Retrofit =
        Retrofit
            .Builder()
            .client(okHttpClient)
            .baseUrl(BASE_URL)
            .addConverterFactory(MoshiConverterFactory.create())
            .build()

    @Provides
    fun provideArticleWs(retrofit: Retrofit): ArticleWs = 
        retrofit.create(ArticleWs::class.java)

    @Provides
    fun articleDataSourceProvider(articleWs: ArticleWs) = 
        ArticleRemoteDataSourceImpl(articleWs) as ArticleRemoteDataSource
}
1

HTTP client

OkHttpClient handles network communication with interceptors for logging and authentication.
2

JSON parsing

Moshi converts JSON responses to Kotlin data classes with Kotlin reflection support.
3

Retrofit instance

Retrofit creates type-safe API interfaces from the base URL and converters.
4

Service creation

Retrofit generates implementations of API interfaces at runtime.

Repository pattern

Repositories coordinate between local and remote data sources:
data/repository/src/main/java/es/mobiledev/data/repository/article/ArticleRepository.kt
class ArticleRepository(
    private val remote: ArticleRemoteDataSource,
    private val local: ArticleLocalDataSource,
) : ArticleGateway {
    override suspend fun getArticles(
        limit: Long,
        offset: Long
    ): Flow<ArticleResponseBo> = flowOf(
        remote.getArticles(limit = limit, offset = offset)
    )

    override suspend fun getArticleById(
        id: Long
    ): Flow<ArticleBo> = flowOf(
        remote.getArticleById(id = id)
    )

    override suspend fun getFavoriteArticles(): Flow<List<ArticleBo>> = 
        flowOf(local.getFavoriteArticles())

    override suspend fun saveFavoriteArticle(articleBo: ArticleBo) = 
        local.saveFavoriteArticle(articleBo)

    override suspend fun removeFavoriteArticle(articleBo: ArticleBo) = 
        local.removeFavoriteArticle(articleBo)

    override suspend fun isArticleFavorite(id: Long) = 
        flowOf(local.isArticleFavorite(id))
}
Repositories implement gateway interfaces from the domain layer, enforcing separation between data and business logic.

Data source pattern

Data sources define interfaces for local and remote operations:
data/source/src/main/java/es/mobiledev/data/source/article/ArticleRemoteDataSource.kt
interface ArticleRemoteDataSource {
    suspend fun getArticles(
        limit: Long,
        offset: Long
    ): ArticleResponseBo

    suspend fun getArticleById(id: Long): ArticleBo
}
Data source interfaces use domain models (BO) as return types, abstracting away DTOs and DBOs.

Data mapping

Each layer maps between its data types:
// data/remote/src/main/java/es/mobiledev/data/remote/article/Mapper.kt
fun ArticleDto.toBo() = ArticleBo(
    id = id,
    title = title,
    authors = authors.map { it.toBo() },
    url = url,
    imageUrl = imageUrl,
    newsSite = newsSite,
    summary = summary,
    publishedAt = publishedAt,
    updatedAt = updatedAt,
)

Data flow

Understand how data flows through the layers:
1

API response

Retrofit receives JSON and converts it to DTOs using Moshi.
2

DTO to BO

Remote data source maps DTOs to business objects (BOs).
3

Repository coordination

Repository decides whether to use local or remote data source.
4

Domain gateway

Repository returns BOs through the gateway interface to the domain layer.
5

Use case processing

Use cases receive BOs and apply business logic.

Caching strategy

Implement caching with local and remote data sources:
override suspend fun getArticles(
    limit: Long,
    offset: Long,
    forceRefresh: Boolean
): Flow<ArticleResponseBo> = flow {
    // Emit cached data first
    if (!forceRefresh) {
        val cached = local.getCachedArticles()
        if (cached.isNotEmpty()) {
            emit(ArticleResponseBo(results = cached))
        }
    }
    
    // Fetch fresh data from network
    try {
        val fresh = remote.getArticles(limit, offset)
        local.cacheArticles(fresh.results)
        emit(fresh)
    } catch (e: Exception) {
        // Continue with cached data if network fails
    }
}
Always handle network errors gracefully and provide fallback to cached data when possible.

Best practices

Single source of truth

Use the local database as the single source of truth and sync with remote.

Reactive data

Return Flow from repositories to support reactive UI updates.

Error handling

Handle network and database errors at the repository level.

Suspend functions

Use suspend functions for asynchronous operations with coroutines.

Dependency injection

Inject data sources and repositories using Hilt for testability.

Type safety

Use sealed classes for API responses and result wrappers.

Build docs developers (and LLMs) love