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
}
HTTP client
OkHttpClient handles network communication with interceptors for logging and authentication.
JSON parsing
Moshi converts JSON responses to Kotlin data classes with Kotlin reflection support.
Retrofit instance
Retrofit creates type-safe API interfaces from the base URL and converters.
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:
Remote data source
Local data source
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/src/main/java/es/mobiledev/data/source/article/ArticleLocalDataSource.kt
interface ArticleLocalDataSource {
suspend fun getFavoriteArticles (): List < ArticleBo >
suspend fun saveFavoriteArticle (article: ArticleBo )
suspend fun removeFavoriteArticle (article: ArticleBo )
suspend fun isArticleFavorite (id: Long ): Boolean
}
Data source interfaces use domain models (BO) as return types, abstracting away DTOs and DBOs.
Data mapping
Each layer maps between its data types:
DTO to BO (Remote)
DBO to BO (Local)
// 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:
API response
Retrofit receives JSON and converts it to DTOs using Moshi.
DTO to BO
Remote data source maps DTOs to business objects (BOs).
Repository coordination
Repository decides whether to use local or remote data source.
Domain gateway
Repository returns BOs through the gateway interface to the domain layer.
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.