Jetpack is the set of Google libraries that solve common app problems. You already used ViewModel, Room, DataStore, WorkManager and Navigation. This lesson adds the rest of the ones you will meet at work.
Room relationships
One-to-many: a user has many orders. Put a userId foreign key in the orders table, and read both with an @Relation class.
@Entity data class User(@PrimaryKey val id: Int, val name: String)
@Entity(foreignKeys = [ForeignKey(User::class, ["id"], ["userId"], onDelete = ForeignKey.CASCADE)])
data class Order(@PrimaryKey(autoGenerate = true) val id: Int = 0, val userId: Int, val total: Long)
data class UserWithOrders(
@Embedded val user: User,
@Relation(parentColumn = "id", entityColumn = "userId") val orders: List<Order>
)
@Dao interface UserDao {
@Transaction
@Query("SELECT * FROM User WHERE id = :id")
fun userWithOrders(id: Int): Flow<UserWithOrders>
}
One-to-one is the same with a single object instead of a list. Many-to-many needs a *join table* (for example StudentCourse) and @Relation(associateBy = Junction(StudentCourse::class)). @Transaction makes sure several reads or writes happen together, or not at all. Mark foreignKeys with CASCADE so deleting a parent removes its children.
Migrations
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE Note ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0")
}
}
Room.databaseBuilder(context, AppDatabase::class.java, "app.db").addMigrations(MIGRATION_1_2).build()
Turn on exportSchema and keep the exported JSON in git so you can write migration tests. Never use fallbackToDestructiveMigration in a released app unless losing user data is acceptable.
Proto DataStore and database encryption
Preferences DataStore stores key-value pairs. Proto DataStore stores a typed object defined by a schema, which prevents key typos. Room itself is not encrypted. For sensitive data, either encrypt the fields you store or use an encrypted SQLite build such as SQLCipher, and think hard about whether you need to store the data at all.
Paging 3
Paging loads a long list in pages as the user scrolls. A PagingSource loads one page (from the network or Room), a Pager turns it into a Flow<PagingData<T>>, and Compose shows it with collectAsLazyPagingItems().
val posts: Flow<PagingData<Post>> = Pager(PagingConfig(pageSize = 20)) { PostSource(api) }
.flow.cachedIn(viewModelScope)
@Composable fun PostList(vm: PostViewModel) {
val items = vm.posts.collectAsLazyPagingItems()
LazyColumn {
items(items.itemCount) { index -> items[index]?.let { PostRow(it) } }
}
}
Navigation: arguments, deep links, nested graphs
- Pass an id, not an object: route
detail/{id}withnavArgument("id") { type = NavType.IntType }. - Add
deepLinks = listOf(navDeepLink { uriPattern = "myapp://item/{id}" })to a destination, and an intent filter in the manifest. - Group screens with
navigation(startDestination = "login", route = "auth") { ... }(nested graph) so you can leave the whole group at once. - For bottom navigation, save and restore state with
launchSingleTop = true; restoreState = trueandpopUpTo(startDestination) { saveState = true }.
SplashScreen, App Startup, Activity Result and CameraX
- SplashScreen API:
installSplashScreen()beforesuper.onCreateshows a consistent launch screen on all Android versions. - App Startup: initialises libraries in a defined order from one place, so many libraries do not each slow startup.
- Activity Result APIs:
rememberLauncherForActivityResultfor permissions, picking photos (PickVisualMedia) and documents. - SavedStateHandle: ViewModel access to navigation arguments and values that survive process death.
- CameraX: a simple camera API with use cases for preview, image capture and analysis (used in the QR project).
Offline-first caching
The recipe: the UI observes Room; a refresh() function fetches from the network and writes into Room; errors only show a message because old data is still visible. Add a lastUpdated time to decide when to refresh. Use WorkManager to sync in the background with a network constraint.
Your task
- Add a one-to-many relation (category and expenses) to project 7.
- Add a migration that adds a column and write a note about how you would test it.
- Use Paging 3 to show 1,000 fake items in a LazyColumn.
- Add a deep link to a detail screen and open it from adb.
How to check it worked
- Relations load with one query.
- A migration keeps old data.
- You can describe the offline-first flow.