Skip to main content

Overview

The offline mode feature enables users to access previously loaded articles even without an internet connection. It uses Room database to cache pagination state and provides a foundation for caching article data.

Architecture

The app uses Room as the local persistence layer to store data that can be accessed offline.
Room Database Components:
  • Database class that extends RoomDatabase
  • DAOs (Data Access Objects) for database operations
  • Entities representing database tables

Database Configuration

The SpaceFlightNewsDb class defines the Room database structure.
SpaceFlightNewsDb.kt:16-20
@Database(entities = [PaginationEntity::class], version = 1)
abstract class SpaceFlightNewsDb: RoomDatabase() {
    abstract fun paginationDao(): PaginationDao
}
The database currently stores pagination state. The architecture can be extended to cache article content for full offline support.

Dependency Injection

The database is configured through Dagger Hilt for dependency injection.
@Module
@InstallIn(SingletonComponent::class)
object RoomModule {
    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): SpaceFlightNewsDb {
        return Room.databaseBuilder(
            context,
            SpaceFlightNewsDb::class.java,
            "space_flight_news_db"
        ).build()
    }

    @Provides
    fun providePaginationDao(database: SpaceFlightNewsDb): PaginationDao {
        return database.paginationDao()
    }
}

Pagination State Caching

The app caches pagination state to remember the user’s scroll position.

PaginationEntity

This entity stores the current pagination state in the database.
@Entity(tableName = "pagination")
data class PaginationEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val count: Int,
    val offset: Int?
)

PaginationDao

The DAO provides methods to interact with pagination data.
@Dao
interface PaginationDao {
    @Query("SELECT * FROM pagination LIMIT 1")
    suspend fun getPagination(): PaginationEntity?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertPagination(pagination: PaginationEntity)

    @Query("DELETE FROM pagination")
    suspend fun deletePagination()
}

PaginationManager Integration

The PaginationManager coordinates between the API and database.

Reading Cached Offset

PaginationManager.kt:17-19
suspend fun getCurrentOffset(): Int? {
    return paginationDao.getPagination()?.offset
}
When the app starts offline, it can retrieve the last known offset from the database.

Writing Pagination State

PaginationManager.kt:24-37
suspend fun updatePagination(paginationDto: PaginationDto) {
    val offset = extractOffset(paginationDto.next)

    paginationDao.deletePagination()

    if (paginationDto.articles.isNotEmpty()) {
        paginationDao.insertPagination(
            PaginationEntity(
                count = paginationDto.count,
                offset = offset
            )
        )
    }
}

Repository Pattern

The repository abstracts data sources and can be extended for offline support.

Current Implementation

ArticleRepositoryImpl.kt:39-58
override suspend fun getArticles(
    query: String?
): Resource<List<Article>> {
    return withContext(Dispatchers.IO) {
        val response: ApiResponse<PaginationDto> =
            apiHelper.safeApiCall {
                api.getArticles(query, paginationManager.getCurrentOffset())
            }

        when (response) {
            is ApiSuccessResponse -> {
                val pagination = response.body
                paginationManager.updatePagination(pagination)
                Resource.Success(pagination.articles.map { it.toArticleDomain() })
            }

            is ApiErrorResponse -> Resource.Error(response.code, response.msg, response.error)
        }
    }
}

Extending for Full Offline Support

To implement full offline caching, the repository can be enhanced:
1

Check Network

Determine if device is online or offline
2

Try Network First

If online, fetch from API and cache results
3

Fallback to Cache

If offline or network fails, retrieve from database
4

Return Data

Provide data regardless of network state
override suspend fun getArticles(query: String?): Resource<List<Article>> {
    return withContext(Dispatchers.IO) {
        // Try network first
        if (networkHelper.isNetworkConnected()) {
            val response = apiHelper.safeApiCall {
                api.getArticles(query, paginationManager.getCurrentOffset())
            }

            when (response) {
                is ApiSuccessResponse -> {
                    val articles = response.body.articles
                    // Cache articles in database
                    articleDao.insertArticles(articles.map { it.toEntity() })
                    paginationManager.updatePagination(response.body)
                    return@withContext Resource.Success(
                        articles.map { it.toArticleDomain() }
                    )
                }
                is ApiErrorResponse -> {
                    // Network error, fallback to cache
                }
            }
        }

        // Fallback to cached data
        val cachedArticles = articleDao.getArticles()
        if (cachedArticles.isNotEmpty()) {
            Resource.Success(cachedArticles.map { it.toDomain() })
        } else {
            Resource.Error(message = "No cached data available")
        }
    }
}

Network Status Detection

The app includes a NetworkHelper to detect connectivity.
class NetworkHelper @Inject constructor(
    @ApplicationContext private val context: Context
) {
    fun isNetworkConnected(): Boolean {
        val connectivityManager = context.getSystemService(
            Context.CONNECTIVITY_SERVICE
        ) as ConnectivityManager
        
        val network = connectivityManager.activeNetwork ?: return false
        val capabilities = connectivityManager.getNetworkCapabilities(network)
        
        return capabilities?.hasCapability(
            NetworkCapabilities.NET_CAPABILITY_INTERNET
        ) == true
    }
}

Database Operations

Room database operations run on background threads using coroutines.

Suspend Functions

All DAO methods use suspend for coroutine support

IO Dispatcher

Database operations run on Dispatchers.IO

Type Safety

Room provides compile-time SQL query verification

LiveData/Flow

Support for reactive data streams

Extending Offline Capabilities

To add full article caching, create additional entities and DAOs:

Article Entity

@Entity(tableName = "articles")
data class ArticleEntity(
    @PrimaryKey
    val id: Long,
    val title: String,
    val imageUrl: String,
    val newsSite: String,
    val publishedAt: String,
    val cachedAt: Long = System.currentTimeMillis()
)

Article DAO

@Dao
interface ArticleDao {
    @Query("SELECT * FROM articles ORDER BY publishedAt DESC")
    suspend fun getArticles(): List<ArticleEntity>

    @Query("SELECT * FROM articles WHERE id = :articleId")
    suspend fun getArticleById(articleId: Long): ArticleEntity?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertArticles(articles: List<ArticleEntity>)

    @Query("DELETE FROM articles WHERE cachedAt < :expirationTime")
    suspend fun deleteExpiredArticles(expirationTime: Long)
}

ArticleDetail Entity

@Entity(tableName = "article_details")
data class ArticleDetailEntity(
    @PrimaryKey
    val id: Long,
    val title: String,
    val url: String,
    val newsSite: String,
    val imageUrl: String,
    val summary: String,
    val publishedAt: String,
    val updatedAt: String
)

Cache Invalidation Strategy

Implement cache expiration to keep data fresh:
class CacheManager @Inject constructor(
    private val articleDao: ArticleDao
) {
    suspend fun clearExpiredCache() {
        val oneDayAgo = System.currentTimeMillis() - (24 * 60 * 60 * 1000)
        articleDao.deleteExpiredArticles(oneDayAgo)
    }
}
Regularly clearing expired cache prevents the database from growing too large and ensures users see relatively fresh content.

Benefits

Persistent State

Pagination position survives app restarts

Offline Access

View previously loaded content without internet

Better UX

Instant loading of cached content

Reduced Data Usage

Less API calls for repeated content

Current Limitations

The current implementation stores only pagination state. Future enhancements could include:
Cache the list of articles for offline browsing
Store full article details including content and images
Use Glide’s disk cache for offline image display
Sync cached data when connection is restored

Pagination

Pagination state is persisted offline

Article List

Can be enhanced with article caching

Article Details

Can be enhanced with detail caching

Build docs developers (and LLMs) love