## Table of contents ## Project description 2 commits, available on GIT - `App.kt` - `appContext` hack - defined in manifest - `manifest` - `windowSoftInputMode="adjustResize` - app `build.gradle` - dependencies - `ksp` - `ksp` schema - root `build.gradle` - `ksp` ## Settings ### SharedPrefs ```kotlin= Column( modifier = Modifier .padding(paddingValues) .padding(12.dp) .fillMaxSize(), ) { Text("Name") Spacer(Modifier.height(4.dp)) TextField( value = "hello", onValueChange = { }, modifier = Modifier.fillMaxWidth(), ) } ``` Save button ```kotlin= Spacer(Modifier.weight(1f)) Button( onClick = { viewModel.onSaveClick() }, modifier = Modifier.fillMaxWidth(), ) { Text("Save") } ``` ```kotlin= fun onNameChange(name: String) { _screenStateStream.update { state -> state.copy(name = name) } } val state by viewModel.screenStateStream.collectAsStateWithLifecycle() ``` - Survives screen rotation :+1: ProfileDataSource ```kotlin= private val prefs = appContext.getSharedPreferences("fit_prefs", Context.MODE_PRIVATE) private const val NAME_KEY = "name" fun getName(): String { } var name: String get() = prefs.getString(NAME_KEY, null) ?: "" set(value) { prefs.edit { putString(NAME_KEY, value) } } ``` Profile viewmodel ```kotlin= fun onSaveClick() { viewModelScope.launch { ProfileDataSource.name = screenStateStream.value.name } } _screenStateStream.update { state -> state.copy( name = ProfileDataSource.name, ) } ``` - They are not stream aware...how? - SharedPrefs listener - MutableStateFlow - How to view preferences - Device file explorer - Flipper - Not stream aware ### Datastore ```kotlin= enum class DarkMode ``` ```kotlin= DarkMode.values().forEach { option -> Button( onClick = { }, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), ) { Text(option.displayName) } } ``` ```kotlin= fun onDarkModeSelection(darkMode: DarkMode) { _screenStateStream.update { state -> state.copy(selectedDarkMode = darkMode) } } ``` ```kotlin= private val Context.dataStore by preferencesDataStore("fit_datastore") private val DARK_MODE_KEY = stringPreferencesKey("name") fun getDarkModeStream(): Flow<DarkMode> { return appContext.dataStore.data .map { prefs -> val savedDarkModeName = prefs[DARK_MODE_KEY] if (savedDarkModeName != null) { DarkMode.valueOf(savedDarkModeName) } else { DarkMode.SYSTEM } } .distinctUntilChanged() } suspend fun setDarkMode(darkMode: DarkMode) { appContext.dataStore.edit { prefs -> prefs[DARK_MODE_KEY] = darkMode.name } } ``` - `distinct` needed! - change VM ```kotlin= val darkMode by ProfileDataSource.getDarkModeStream().collectAsStateWithLifecycle(null) darkMode?.let { Lecture7Theme(isInDarkTheme(it)) { Navigation() } } ``` - What if we wanted to save some complicated object ## Notes ```kotlin= @Entity(tableName = "notes") data class Note( @PrimaryKey(autoGenerate = true) val id: Int = 0, @ColumnInfo(name = "textik") val text: String ) ``` ```kotlin= @Entity(primaryKeys = ["id", "text"]) @Entity(tableName = "notes") @ColumnInfo(name = "textik") ``` - Mention `@Ignore` ```kotlin= @Dao interface NotesDao { @Query("SELECT * FROM notes") fun getAll(): List<Note> } @Database(entities = [Note::class], version = 1) abstract class CvutDatabase : RoomDatabase() { abstract fun notesDao(): NotesDao } ``` ```kotlin= object NotesDataSource { private val database = Room.databaseBuilder( appContext, CvutDatabase::class.java, "fit_db" ).build() private val notesDao = database.notesDao() } ``` - What is ksp - Show generated: `build -> generated -> ksp` - Show that schema was generated ### Select basic ```kotlin= LazyColumn( contentPadding = PaddingValues(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .padding(paddingValues) .fillMaxSize(), ) { items( state.notes, key = { note -> note.id }, ) { note -> ``` ```kotlin= init { val notes = NotesDataSource.getAllNotesStream() _screenStateStream.update { it.copy(notes = notes) } } ``` - Show coroutine - Switch to dispatcher - Use suspend function DataSource ```kotlin= fun getAllNotes(): List<Note> { return notesDao.getAll() } ``` ### Insert ```kotlin= @Insert suspend fun insert(note: Note) suspend fun insertNote(note: Note) { return notesDao.insert(note) } ``` ```kotlin= @Insert(onConflict = OnConflictStrategy.REPLACE) ``` - It's all based on the primary key - Show what happens on rotation - Database inspector ```kotlin= Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(4.dp), elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) ) { ``` ### Select stream - Can do coroutines, rxjava, livedata ```kotlin= @Query("SELECT * FROM notes ORDER BY id DESC") fun getAllStream(): Flow<List<Note>> fun getAllNotesStream(): Flow<List<Note>> { return notesDao.getAllStream() } NotesDataSource.getAllNotesStream() .onEach { notes -> _screenStateStream.update { state -> state.copy(notes = notes) } } .launchIn(viewModelScope) ``` ```kotlin= @Query("SELECT * FROM notes WHERE id > :threshold ORDER BY id DESC") fun getAllStream(threshold: Int): Flow<List<Note>> ``` ### Delete ```kotlin= suspend fun deleteNote(note: Note) { notesDao.delete(note) } @Delete suspend fun delete(note: Note) ``` ### Update - Same as delete ### Delete all - Show that you can't use `@Delete` ```kotlin= @Query("DELETE FROM notes") suspend fun deleteAll() suspend fun deleteAllNotes() { notesDao.deleteAll() } ``` - We can insert more things - We can return id of the row or number of affected rows - Database can do many things, `JOIN`, `@Embedded`, modelling of relations etc ```kotlin= @Transaction public void insertAndDeleteInTransaction(Product newProduct, Product oldProduct) { // Anything inside this method runs in a single transaction. insert(newProduct); delete(oldProduct); } ``` ### Migrations - Whenever schema changes, version needs to be bumped - Automatic and manual migrations #### Manual migration ```kotlin= @Entity(tableName = "notes") data class Note( @PrimaryKey(autoGenerate = true) val id: Int = 0, @ColumnInfo(name = "textik") val text: String, val name: String = "" ) ``` - Try to run ```kotlin= @Database( entities = [Note::class], version = 2, exportSchema = true, autoMigrations = [AutoMigration(from = 1, to = 2)] ) ``` - Try to run ```kotlin= .addMigrations( object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE notes ADD COLUMN subtitle TEXT NOT NULL DEFAULT ''") } } ) ``` #### Automatic - Just mention #### `fallbackToDestructiveMigration`