Database 3
Database 2 - Main View with Room - Continued
Step 0: Check the error message after the app is crashed
Step 1: Create an application class to store database and repository
- check ContactsViewModel, we need a repository
- we create the ContactsViewModel by calling viewModel() in ContactUI.kt. Right-click on ContactsViewModel -> go to -> declaration or usage. To fix it, we need to pass a dao somehow. Go to ContactDatabase, we want to have a single instance for it. For Android, we can use so-called application class.
- Go to app -> manifests We can have many activities, but one application
android:theme="@style/Theme.ContactsApp"
android:name=".ContactsApplication">
<!-- The . before the class name means relative to your app’s package name.
So if your app’s package is com.example.mycontacts, then .ContactsApplication means
-> com.example.mycontacts.ContactsApplication.
-->
- We take the name and create a kotlin class with exactly the same name under the main package. The class must extend the Application to guarantee that it has one instance.
in ContactsApplication.kt
package at.uastw.contactsapp
import android.app.Application
import at.uastw.contactsapp.data.ContactRepository
import at.uastw.contactsapp.db.ContactsDatabase
// The Application class lives for the entire lifetime of the app, making it
// the perfect place to create singletons like your Room database, DAO, and Repository.
// This ensures they are created only once, reused everywhere, and not recreated
// every time an Activity or ViewModel is created.
class ContactsApplication : Application() { // only one instance
// by lazy: only execute this code once, once it is accessed somewhere
// Don’t create this value until actually needed.
// The code inside { ... } does NOT run immediately. It only runs the first time
// you access the property. After that, the result is cached and reused.
// The block will run once only.
val contactRepository by lazy {
// create database (via getDatabase(this)) and DAO
// contactsDao() is an abstract function,
// which ask Room to auto-generates the corresponding implementation
val contactsDao = ContactsDatabase.getDatabase(this).contactsDao()
ContactRepository(contactsDao)
}
}
Why we need an application class?
- Creating a Room database is expensive.
- Creating a DAO is cheap but requires the database first.
- Repository should be initialized once and reused.
UI
- Activity -> destroyed & recreated often (rotation, navigation)
- Fragment -> also often recreated
- ViewModel -> scoped to UI screens (navigation destinations)
- Application -> created once per app launch
Step 2: Create an AppViewModelProvider, allowing us to create multiple view models
It is a convenient way to define your custom ViewModel factory in one place, so it can be used anywhere in your app.
- Connect this application somehow with our viewmodel. In viewModel(), we need to provide a factory. factory is an object that creates another object.
- Add New -> Kotlin class -> Object and name it AppViewModelProvider (put it under ui, because viewmodel belongs to the ui layer)
in AppViewModelProvider.kt
package at.uastw.contactsapp.ui
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import at.uastw.contactsapp.ContactsApplication
object AppViewModelProvider {
// Creates a ViewModel factory that you can pass to your ViewModelProvider.
// A ViewModelFactory in Kotlin (specifically Android) is a class whose job is
// to create ViewModel instances, especially when a ViewModel needs constructor parameters.
val Factory = viewModelFactory {
// Inside the viewModelFactory, you define one or more initializers.
// Each initializer describes how to create a specific ViewModel type.
// Main View Model
initializer {
// this[APPLICATION_KEY]
// Inside an initializer, this refers to a CreationExtras object.
// CreationExtras is like a small key-value map containing extra context
// that the system passes to the factory.
// One of these keys is APPLICATION_KEY, which stores the current Application object.
val contactsApplication = this[APPLICATION_KEY] as ContactsApplication
// create the ContactsViewModel
ContactsViewModel(contactsApplication.contactRepository)
}
// ViewModel per screen (Fragment/Activity/Compose destination).
// This is the cleanest and most scalable structure.
// Detailed View Model
}
}
Step 3: Pass the Factory to the viewModel
- Go to ContactsUI.kt
By default, ViewModel constructors can only take the Application or no parameters at all. If you need to pass dependencies like a repository, you need a factory.
@Composable
fun ContactsApp(
modifier: Modifier = Modifier,
contactsViewModel: ContactsViewModel = viewModel(factory = AppViewModelProvider.Factory)) {
// ...
}
Until here, the program should run without problems.
- From UI explorer -> more tool windows -> add app inspection. Select the current app and check database inspector. Double click on contacts, the it should show the current data and the customzied field name. Since we use a FLow in Dao, we can update the name in the inspector. Observe that the app UI is also updated.
Database 3 - Secondary View with Room
Step 1: Create a detailed view model
- Create a detailed view model, under package ui, Add New -> Kotline class and name it ContactDetailViewModel
in ContactDetailViewModel.kt
package at.uastw.contactsapp.ui
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import at.uastw.contactsapp.data.ContactRepository
class ContactDetailViewModel(
private val savedStateHandle: SavedStateHandle,
private val repository: ContactRepository) : ViewModel()
{
// variable use to read contactId, which will be added to the nav controller
// ?: takes the right value if the left value is null
private val contactId: Int = savedStateHandle["contactId"] ?: 0
}
SavedStateHandle is a key-value storage object provided to a ViewModel that:
- Holds navigation arguments
- Saves and restores screen state
- Survives process death
- Works like a small, type-safe dictionary
- Add another initialization to the AppViewModelProvider
in AppViewModelProvider.kt
object AppViewModelProvider {
val Factory = viewModelFactory {
initializer {
val contactsApplication = this[APPLICATION_KEY] as ContactsApplication
ContactsViewModel(contactsApplication.contactRepository)
}
initializer {
val contactsApplication = this[APPLICATION_KEY] as ContactsApplication
// this.createSavedStateHandle() is part of Jetpack ViewModel initialization,
// specifically when using the new viewModelFactory { initializer { ... } } API.
// You only see this call inside a ViewModel factory initializer, not in normal code.
// this is a CreationExtras receiver that has helper functions
// createSavedStateHandle(): navigation arguments, previously saved state, default saved state storage
ContactDetailViewModel(
this.createSavedStateHandle(),
contactsApplication.contactRepository)
}
}
}
- Create a new composible function in ContactsUI.kt and add it before @Composable fun ContactDetails(contact: Contact, modifier: Modifier = Modifier)
in ContactsUI.kt
@Composable
fun ContactDetailView(contactDetailViewModel: ContactDetailViewModel = viewModel(factory = AppViewModelProvider.Factory)) {
// To read the contact we selected
// val contact = contactDetailViewModel.
}
but sth is still missing, we cannot continue On the top of the same file, we need to rename
@Composable
fun ContactsApp( modifier: Modifier = Modifier,
contactsViewModel: ContactsViewModel = viewModel(factory = AppViewModelProvider.Factory))
// to
@Composable
fun ContactsListView( modifier: Modifier = Modifier,
contactsViewModel: ContactsViewModel = viewModel(factory = AppViewModelProvider.Factory))
add a new composible function called ContactsApp() and before that create a enum class called Routes{}
// newly add
enum class Routes{
List,
Detail
}
// modifier (name): Modifier (type) = Modifier (default value),
// newly add
@Composable
fun ContactsApp(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController()
) {
// if anything red above, add dependency and import
// Displays the current screen depending on navigation state
NavHost(
navController = navController, modifier = modifier, startDestination = Routes.List.name
) {
// composable(route) { ... } defines a destination in the NavHost.
composable(Routes.List.name) {
ContactsListView()
}
composable(Routes.Detail.name) {
ContactDetailView()
}
}
}
@Composable
fun ContactsListView( modifier: Modifier = Modifier,
contactsViewModel: ContactsViewModel = viewModel(factory = AppViewModelProvider.Factory))
To make sure that we get the unique name in the data shown in UI, we need to pass id into route, called navigation archive?
- add a parameter to class Routes()
- change Routes.List.name -> Routes.List.route
- add a function parameter to ContactListView()
- update the parameter passes to ContactListView() in ContactsApp()
// the route property serves as the navigation path for each screen in Jetpack Compose Navigation.
// route defines how the NavController identifies the destination.
// It replaces hardcoded strings like "list" or "detail/{contactId}" with a centralized, type-safe definition.
// Each enum value corresponds to a screen (or destination) in your navigation graph.
enum class Routes(val route: String) {
List("list"),
Detail("detail/{contactId}") // The {contactId} is a path parameter (placeholder) for navigation.
}
@Composable
fun ContactsApp(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController()
) {
// if anything red above, add dependency and import
// change name to route
// Routes.Detail.name // "Detail"
// Routes.Detail.route // "detail/{contactId}", a customized property
NavHost(
navController = navController, modifier = modifier, startDestination = Routes.List.route
) {
composable(Routes.List.route) {
// The lambda is called when the user clicks a contact.
// Dynamically navigates to the Detail screen,
// replacing {contactId} in the route with the actual ID.
// If contactId = 5, it navigates to "detail/5".
// Now ContactsListView carries a function, we need to update it
ContactsListView(){
contactId -> navController.navigate("detail/$contactId")
}
}
// to get contactId, we need to set the second parameter
// 2nd arg: declares the argument contactId and its type (Int).
// This tells Compose Navigation to expect this parameter and parse it
// automatically from the route
composable(Routes.Detail.route, listOf(navArgument("contactId"){
type = NavType.IntType
})) {
ContactDetailView()
}
}
}
@Composable
fun ContactsListView(
modifier: Modifier = Modifier,
contactsViewModel: ContactsViewModel = viewModel(factory = AppViewModelProvider.Factory),
onContactClick: (Int) -> Unit
)
-
(Int) -> Unit → type: a function that takes an Int as input and returns nothing (Unit is like void in Java)
-
User is on the List screen (“list”).
- Clicks a contact with contactId = 5.
- navController.navigate(“detail/5”) is called.
- NavHost finds the route “detail/{contactId}”.
- Compose Navigation extracts the argument (contactId = 5).
- The Detail Composable is shown, receiving contactId.
- Go to fun ContactsListView() and add
LazyColumn {
itemsIndexed(state) { index, contact ->
ContactListItem(contact, onCardClick = {
// newly add
onContactClick(contact.id)
// contactsViewModel.onCardClick(index)
})
}
}
- check contactId are the same in ContactsUI.kt and ContactDetailViewModel.kt
Step 2: Create a detailed view model - State
- In ContactDetailViewModel.kt, add ContactDetailUiState()
in ContactDetailViewModel.kt
data class ContactDetailUiState(
val contact: Contact
)
class ContactDetailViewModel(
private val savedStateHandle: SavedStateHandle,
private val repository: ContactRepository) : ViewModel() {
// Use to read contactId and is added to the nav controller
private val contactId: Int = savedStateHandle["contactId"] ?: 0
// newly add below
// The initial value is a ContactDetailUiState wrapping a blank/default Contact.
// MutableStateFlow allows changing the value (value = ..., .update { ... }, etc.),
// which do not allow UI (Compose screens) to modify ViewModel state directly.
private val _contactDetailUiState = MutableStateFlow(ContactDetailUiState(
Contact(0,"", "", 0)
))
// asStateFlow() converts the mutable flow into a read-only StateFlow
val contactDetailUiState = _contactDetailUiState.asStateFlow()
init{
// Distribute to coroutine
viewModelScope.launch {
val contact = repository.findContactById(contactId)
_contactDetailUiState.update {
// copy data found in the repository to UiState
it.copy(contact = contact)
}
}
}
}
- Go to ContactRepository.kt and at the end of the file add findContactById()
suspend fun addRandomContact() {
contactsDao.addContact(
ContactEntity(0, names.random(), "+4357894", 45)
)
}
// newly add
suspend fun findContactById(contactId: Int): Contact {
val contactEntity = contactsDao.findContactById(contactId)
return Contact(
contactEntity.id, contactEntity.name, contactEntity.telephoneNumber, contactEntity.age
)
}
- Go to ContactsUI.kt and update ContactDetailView()
@Composable
fun ContactDetailView(contactDetailViewModel: ContactDetailViewModel = viewModel(factory = AppViewModelProvider.Factory)) {
// To read the contact we selected
val state by contactDetailViewModel.contactDetailUiState.collectAsStateWithLifecycle()
ContactDetails(state.contact)
}
Until now, the detailed view should be working. To test it, by default, it is using the Gesture Navigation Mode. This means that when using the smulator, we need to
- Mouse: press mouse left button and swip from the right side of the emulator
- Touch pad: press the touch pad and swip from the right side of the emulator (with one finger). Only works with Pixel 9a (API 36) on my machine, but Pixel 6a (API35) and Medium Phone API 36 do not work. No idea why.
Potential Errors
- Inconsistent database version
- Remove existing database
- Open Android Studio -> View -> Tool Windows -> Device File Explorer.
- Navigate to: /data/data/
/databases/ - Right-click your database file (e.g., app_database)-> Delete.
- Remove existing database
-
apk-build-fails-with-error-failed-to-open-apk-inconsistent-information: Solution: File -> Invalidate caches & restart This is because Android Studio may keep some information to speed up the build process.
- When IDE’s internal caches or indexing are corrupted: Solution: Android Studio -> File -> Invalidate Caches… -> Invalidate & Restart. To enforce to rebuild all the indexing, one can also tick to remove all optional items
Terminology
-
Application class: The Application class in Android is the base class within an Android app that contains all other components such as activities and services. The Application class, or any subclass of the Application class, is instantiated before any other class when the process for your application/package is created. This class is primarily used for initialization of global state before the first Activity is displayed.
-
Context: Interface to global information about an application environment. This is an abstract class whose implementation is provided by the Android system. It allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc.
-
as operator: The as operator in Kotlin is used for type casting — converting one type to another (usually casting a superclass/parent reference to a more specific type or interface).
-
object: defines a singleton. In Kotlin, object means: “Create one and only one instance of this class automatically.”
| Feature | object |
companion object |
|---|---|---|
| What it creates | A singleton object | A singleton inside a class |
| Scope | Exists on its own | Belongs to a class |
| How to access | MyObject.method() |
MyClass.method() (no object name needed) |
| Replaces | Java static singleton | Java static members |
| Can implement interfaces | Yes | Yes |
| Can inherit from classes? | No (but can extend abstract class) | No |
| Use case | Global singletons (e.g., object Logger) |
Static-like members inside a class (factory methods, constants) |
-NavController is the main navigation object in Android. It: Manages the back stack, Knows the current destination, Executes navigation actions, and Stores navigation arguments
| Component | What it is | Used for | Who creates it? | Example |
|---|---|---|---|---|
| NavHost | A Composable | Displays the current screen depending on navigation state | You (in UI code) | NavHost(navController, startDestination) |
| NavController | Core navigation controller | Navigate between screens, manage back stack | You create it via rememberNavController() |
navController.navigate("details/5") |
| NavHostController | A subclass of NavController used in Compose | Same as NavController, but with extra state for Compose | Created by rememberNavController() |
Returned by rememberNavController() |
- Factory: Factory Method is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.
Reflection
- One can move database package and remote package under the data pacakage
- Entity should be used only in the repository