Learn Jetpack Compose - Data Persistence & Offline
Episode 11 of 23

Learn Jetpack Compose - Data Persistence & Offline

This episode makes the application work offline: Room integration with Flow, Preferences DataStore for preferences, state restoration with rememberSaveable and SavedStateHandle, and offline-first patterns with a repository layer.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

A good application stays useful without a connection. Episode 11 equips you with local data storage: Room for relational databases, DataStore for preferences, and restoration patterns so state doesn't disappear when the application is recreated.

In Compose, storage and UI meet through Flow: the database exposes a data stream, the UI collects it, and changes are reflected immediately. This is the data-driven UI pattern that also shapes the offline-first architecture.

Episode 11 covers Room, DataStore, state restoration, and the repository layer.

Room Integration with Compose

Entity, DAO, and Database

Room stores relational data in SQLite with type safety. Define an Entity, a DAO, and a Database:

KotlinRoom entity dan DAO
@Entity(tableName = "tugas")
data class Tugas(
    @PrimaryKey val id: Long,
    val judul: String,
    val selesai: Boolean = false
)
 
@Dao
interface TugasDao {
    @Insert
    suspend fun tambah(tugas: Tugas)
 
    @Query("SELECT * FROM tugas ORDER BY id DESC")
    fun semuaTugas(): Flow<List<Tugas>>
}

@Dao with a Flow<List<Tugas>> return type keeps the UI always in sync: every table change emits new data. Room annotations like @PrimaryKey are processed by KSP during the Gradle build.

Room and KSP

Room uses KSP to process annotations. Add the plugin and dependencies in the Gradle build:

KotlinRoom dengan KSP
plugins {
    id("com.google.devtools.ksp")
}
 
dependencies {
    implementation("androidx.room:room-runtime:2.7.0")
    implementation("androidx.room:room-ktx:2.7.0")
    ksp("androidx.room:room-compiler:2.7.0")
}

androidx.room:room-ktx provides coroutines and Flow support, and ksp runs the code generation. After syncing, you can check the configuration is smooth with ./gradlew :app:build.

Preferences DataStore

Storing Simple Preferences

For small key-value pairs like theme preferences, use Preferences DataStore:

KotlinPreferences DataStore
val Context.dataStore by preferencesDataStore(name = "preferensi")
 
val darkModePref: Flow<Boolean> = context.dataStore.data
    .map { it[prefsKeyDarkMode] ?: false }

preferencesDataStore(name = "preferensi") creates a DataStore instance, and dataStore.data is a Flow that can be collected in Compose. The value read can directly drive the theme from episode 6.

State Restoration

rememberSaveable in Compose

In episode 5 you used rememberSaveable for a single value. For complex objects, register a Saver:

KotlinSaver custom
val DaftarSaver = listSaver<DaftarTugas, String>(
    save = { it.items },
    restore = { DaftarTugas(it) }
)
 
var daftar by rememberSaveable(saver = DaftarSaver) {
    mutableStateOf(DaftarTugas(emptyList()))
}

A Saver converts an object into a value storable in a Bundle and restores it again. listSaver saves the list contents and restore rebuilds the object when the Activity is recreated.

SavedStateHandle in the ViewModel

For ViewModel state, SavedStateHandle stores values across process death:

KotlinSavedStateHandle
class FormViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    var nama: String
        get() = savedStateHandle["nama"] ?: ""
        set(value) { savedStateHandle["nama"] = value }
}

savedStateHandle["nama"] is automatically restored after recreation. Combining rememberSaveable in the UI and SavedStateHandle in the ViewModel builds complete restoration.

Offline-first with a Repository Layer

Repository as the Single Source

The offline-first pattern: the UI reads from the repository, and the repository decides between local or network data:

KotlinRepository pattern
class ArtikelRepository(
    private val lokal: ArtikelDao,
    private val remote: ArtikelApi
) {
    fun semuaArtikel(): Flow<List<Artikel>> =
        lokal.semuaArtikel().flatMapLatest { lokalList ->
            if (lokalList.isEmpty()) {
                refreshDariJaringan()
            }
            lokal.semuaArtikel()
        }
 
    private suspend fun refreshDariJaringan() {
        val remoteList = remote.fetch()
        lokal.upsert(remoteList)
    }
}

The repository hides the data source from the UI. flatMapLatest triggers a refresh when the cache is empty, and network results are stored in Room — once stored, the list shows offline.

Data-driven UI

The ViewModel exposes the repository's Flow, and the UI collects it using the pattern from episode 10:

KotlinData-driven UI
val artikel by viewModel.artikelFlow.collectAsStateWithLifecycle()
LazyColumn {
    items(artikel, key = { it.id }) { item ->
        KartuArtikel(item)
    }
}

collectAsStateWithLifecycle connects Room, the repository, and the UI. Because every data layer flows as a Flow, online and offline both render smoothly.

Closing

Episode 11 built persistence: Room with Flow and KSP for relational data, Preferences DataStore for preferences, state restoration with Saver and SavedStateHandle, and the offline-first pattern with a repository that decides the data source and makes the UI data-driven.

Key takeaways:

  • A DAO with a Flow return type keeps the UI always in sync with the database.
  • Room uses KSP to process annotations.
  • DataStore handles small key-value preferences.
  • rememberSaveable with a Saver stores complex objects.
  • SavedStateHandle restores ViewModel state across process death.
  • A repository separates the data source and supports offline-first.

In episode 12 we will discuss animations and motion — animate*AsState for smooth transitions, AnimatedVisibility and updateTransition, physics-based animations, and UI polish with gestures.

Learn Jetpack Compose - Data Persistence & Offline | Learn Jetpack Compose