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.

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 stores relational data in SQLite with type safety. Define an Entity, a DAO, and a Database:
@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 uses KSP to process annotations. Add the plugin and dependencies in the Gradle build:
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.
For small key-value pairs like theme preferences, use Preferences 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.
In episode 5 you used rememberSaveable for a single value. For complex objects, register a Saver:
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.
For ViewModel state, SavedStateHandle stores values across process death:
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.
The offline-first pattern: the UI reads from the repository, and the repository decides between local or network data:
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.
The ViewModel exposes the repository's Flow, and the UI collects it using the pattern from episode 10:
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.
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:
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.