Slide 1

Slide 1 text

Let us play in Room with Kotlin @Alex_Zhukovich

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

val db = dbHelper.readableDatabase // Define a projection that specifies which columns from the database // you will actually use after this query. val projection = arrayOf(BaseColumns._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_SUBTITLE) // Filter results WHERE "title" = 'My Title' val selection = "${FeedEntry.COLUMN_NAME_TITLE} = ?" val selectionArgs = arrayOf("My Title") // How you want the results sorted in the resulting Cursor val sortOrder = "${FeedEntry.COLUMN_NAME_SUBTITLE} DESC" val cursor = db.query( FeedEntry.TABLE_NAME, // The table to query projection, // The array of columns to return (pass null to get all) selection, // The columns for the WHERE clause selectionArgs, // The values for the WHERE clause null, // don't group the rows null, // don't filter by row groups sortOrder // The sort order ) https://developer.android.com/training/data-storage/sqlite.html#kotlin

Slide 6

Slide 6 text

val db = dbHelper.readableDatabase // Define a projection that specifies which columns from the database // you will actually use after this query. val projection = arrayOf(BaseColumns._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_SUBTITLE) // Filter results WHERE "title" = 'My Title' val selection = "${FeedEntry.COLUMN_NAME_TITLE} = ?" val selectionArgs = arrayOf("My Title") // How you want the results sorted in the resulting Cursor val sortOrder = "${FeedEntry.COLUMN_NAME_SUBTITLE} DESC" val cursor = db.query( FeedEntry.TABLE_NAME, // The table to query projection, // The array of columns to return (pass null to get all) selection, // The columns for the WHERE clause selectionArgs, // The values for the WHERE clause null, // don't group the rows null, // don't filter by row groups sortOrder // The sort order ) SELECT id, title, subTitle FROM Feed WHERE title = ? ORDER BY subTitle DESC https://developer.android.com/training/data-storage/sqlite.html#kotlin

Slide 7

Slide 7 text

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { if (oldVersion == DATABASE_VERSION_1) { ... } if (oldVersion == DATABASE_VERSION_2) { ... } if (oldVersion == DATABASE_VERSION_3) { ... } if (oldVersion == DATABASE_VERSION_4) { ... } ... }

Slide 8

Slide 8 text

DBFlow SQLBrite GreenDao Realm Room etc.

Slide 9

Slide 9 text

val db = dbHelper.readableDatabase // Define a projection that specifies which columns from the database // you will actually use after this query. val projection = arrayOf(BaseColumns._ID, EmployeeEntry.COLUMN_NAME, EmployeeEntry.COLUMN_PHONE) // Filter results WHERE "name" = 'Alex' val selection = "${EmployeeEntry.COLUMN_NAME} = ?" val selectionArgs = arrayOf("Alex") // How you want the results sorted in the resulting Cursor val sortOrder = "${EmployeeEntry.COLUMN_NAME} DESC" val cursor = db.query( EmployeeEntry.TABLE_NAME, // The table to query projection, // The array of columns to return (pass null to get all) selection, // The columns for the WHERE clause selectionArgs, // The values for the WHERE clause null, // don't group the rows null, // don't filter by row groups sortOrder // The sort order ) SELECT id, name, phone FROM Employee WHERE name = ? ORDER BY name DESC

Slide 10

Slide 10 text

"SELECT * FROM employees WHERE team_id = :teamId"

Slide 11

Slide 11 text

@Query("SELECT * FROM employees WHERE team_id = :teamId")

Slide 12

Slide 12 text

@Query("SELECT * FROM employees WHERE team_id = :teamId") fun getEmployeesByTeamId(): List

Slide 13

Slide 13 text

@Query("SELECT * FROM employees WHERE team_id = :teamId") fun getEmployeesByTeamId(teamId: Long): List

Slide 14

Slide 14 text

@Dao interface EmployeeDao { @Query("SELECT * FROM employees WHERE team_id = :teamId") fun getEmployeesByTeamId(teamId: Long): List }

Slide 15

Slide 15 text

@Dao interface EmployeeDao { @Query("SELECT * FROM employees WHERE team_id = :teamId") fun getEmployeesByTeamId(teamId: Long): List } @Entity data class Employee( @PrimaryKey val teamId: Long, val name: String, val phone: String)

Slide 16

Slide 16 text

@Database(entities = [Employee::class], version = 1) abstract class ContactsDatabase: RoomDatabase() { abstract fun employeesDao(): EmployeesDAO } @Dao interface EmployeeDao { @Query("SELECT * FROM employees WHERE team_id = :teamId") fun getEmployeesByTeamId(): List }

Slide 17

Slide 17 text

Room.databaseBuilder(context.applicationContext, ContactsDatabase::class.java, DATABASE_NAME).build() @Database(entities = [Employee::class], version = 1) abstract class ContactsDatabase: RoomDatabase() { abstract fun employeesDao(): EmployeesDAO }

Slide 18

Slide 18 text

@Insert fun insert(employee: Employee): Long @Insert fun insertListOfEmployees(employees: List) @Insert fun insertEmployees(vararg employee: Employee): List

Slide 19

Slide 19 text

@Update fun updateEmployee(employee: Employee): Int @Update fun updateEmployees(employees: List): Int @Update fun updateEmployee(employee: Employee)

Slide 20

Slide 20 text

@Delete fun deleteEmployee(employee: Employee): Int @Delete fun deleteEmployee(employees: List) @Delete fun deleteEmployee(vararg employee: Employee): Int

Slide 21

Slide 21 text

onConflict = ABORT FAIL IGNORE REPLACE ROLLBACK

Slide 22

Slide 22 text

@Insert(onConflict = OnConflictStrategy.REPLACE) fun insertEmployee(employee: Employee) @Update(onConflict = OnConflictStrategy.ROLLBACK) fun updateEmployee(employee: Employee)

Slide 23

Slide 23 text

Compile time errors

Slide 24

Slide 24 text

@Query("SELECT id, surname FROM employee") fun getEmployees(): List There is a problem with the query: [SQLITE_ERROR] SQL error or missing database ( no such column: surname) @Entity data class Employee( @PrimaryKey val id: Long, val name: String)

Slide 25

Slide 25 text

@Query("SELECT * FROM employee") fun getEmployees(): List There is a problem with the query: [SQLITE_ERROR] SQL error or missing database ( no such table: employee) @Entity(tableName = "employees") data class Employee( @PrimaryKey val id: Long, val name: String)

Slide 26

Slide 26 text

@Dao interface EmployeesDAO { @Query("SELECT name FROM employees") fun getEmployees(): List } @Entity(tableName = "employees") data class Employee( @PrimaryKey val id: Long, val name: String) Columns returned by the query: name. Fields in Employee: id, name.

Slide 27

Slide 27 text

@Dao interface EmployeesDAO { @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) @Query("SELECT name FROM employees") fun getEmployees(): List }

Slide 28

Slide 28 text

@Dao interface EmployeesDAO { @Query("SELECT name FROM employees") fun getEmployees(): List }

Slide 29

Slide 29 text

Entity

Slide 30

Slide 30 text

@Entity data class Employee( val id: Long, val name: String, val phone: String) An entity must have at least 1 field annotated with @PrimaryKey

Slide 31

Slide 31 text

@Entity data class Employee( @PrimaryKey val id: Long, val name: String, val phone: String)

Slide 32

Slide 32 text

@Entity data class Employee( val name: String, val phone: String) { @PrimaryKey(autoGenerate = true) var id: Long? = 0 }

Slide 33

Slide 33 text

@Entity(tableName = "employees") data class Employee( @PrimaryKey val id: Long, val name: String, val phone: String)

Slide 34

Slide 34 text

@Entity( tableName = "employees", primaryKeys = ["id", "name"]) data class Employee( val name: String, val phone: String)

Slide 35

Slide 35 text

@Entity(tableName = "employees") data class Employee( @ColumnInfo(name = "employee_id") val id: Long, @ColumnInfo(name = "full_name") var name: String, @ColumnInfo(name = "phone_number") var phone: String)

Slide 36

Slide 36 text

@Entity(tableName = "employees") data class Employee( @ColumnInfo(name = "employee_id") val id: Long, @ColumnInfo(name = "full_name") var name: String, @ColumnInfo(name = "phone_number") var phone: String) { @Ingore var image: Bitmap? = null }

Slide 37

Slide 37 text

@Entity( tableName = "employees", foreignKeys = [(ForeignKey( entity = Team::class, parentColumns = arrayOf("id"), childColumns = arrayOf("team_id"), onDelete = ForeignKey.CASCADE))]) data class Employee( @ColumnInfo(name = "team_id") var teamId: Long, @ColumnInfo(name = "name") var name: String, @ColumnInfo(name = "phone") var phone: String) { @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "employee_id") var id: Long? = 0 }

Slide 38

Slide 38 text

@Entity(tableName = "employees") data class Employee( @ColumnInfo(name = "name") var name: String, @ColumnInfo(name = "phone") var phone: String, @Embedded val homeLocation: Location) { @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "employee_id") var id: Long? = 0 } id name phone latitude longitude data class Location( val latitude: Double, val longitude: Double)

Slide 39

Slide 39 text

@Entity(tableName = "employees") data class Employee( @ColumnInfo(name = "name") val name: String, @ColumnInfo(name = "phone") val phone: String, @Embedded(prefix = "home_") val homeLocation: Location, @Embedded(prefix = "work_") val workLocation: Location){ @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "employee_id") var id: Long? = 0 } id name phone home_latitude home_longitude work_latitude work_longitude data class Location( val latitude: Double, val longitude: Double)

Slide 40

Slide 40 text

Type Converters

Slide 41

Slide 41 text

@Entity(tableName = "employees") data class Employee( @ColumnInfo(name = "name") val name: String, @ColumnInfo(name = "phone") val phone: String, @ColumnInfo(name = "hire_date") val hireDate: Date) { @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "employee_id") var id: Long? = 0 } Cannot figure out how to save this field into database. You can consider adding a type converter for it. private Date hireDate;

Slide 42

Slide 42 text

object DateConverter { @TypeConverter @JvmStatic fun toDate(value: Long): Date { return Date(value) } @TypeConverter @JvmStatic fun toLong(value: Date): Long { return value.time } }

Slide 43

Slide 43 text

@Database( entities = [Team::class, Employee::class], version = 1) @TypeConverters(DateConverter::class) abstract class ContactsDatabase: RoomDatabase() { abstract fun teamsDao(): TeamsDAO abstract fun employeesDao(): EmployeesDAO }

Slide 44

Slide 44 text

Testing

Slide 45

Slide 45 text

No content

Slide 46

Slide 46 text

adb shell Instrumentation am instrument Application Test Application

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

@RunWith(RobolectricTestRunner::class) class EmployeeDaoTest { … lateinit var database: ContactsDatabase @Before fun setUp() { database = Room.inMemoryDatabaseBuilder( RuntimeEnvironment.application.baseContext, ContactsDatabase::class.java) .allowMainThreadQueries() .build() } @Test fun shouldInsertAndRetrieveEmployee() { database.teamsDao().insertTeam(testTeam) database.employeesDao().insertEmployee(testEmployee) val employees = database.employeesDao().getEmployeeListByTeamId(TEAM_ID) assertEquals(1, employees.size) assertEquals(testEmployee, employees.first()) } @After fun tearDown() { database.close() } }

Slide 50

Slide 50 text

@RunWith(RobolectricTestRunner::class) class EmployeeDaoTest { … lateinit var database: ContactsDatabase @Before fun setUp() { database = Room.inMemoryDatabaseBuilder( RuntimeEnvironment.application.baseContext, ContactsDatabase::class.java) .allowMainThreadQueries() .build() } @Test fun shouldInsertAndRetrieveEmployee() { database.teamsDao().insertTeam(testTeam) database.employeesDao().insertEmployee(testEmployee) val employees = database.employeesDao().getEmployeesByTeamId(TEAM_ID) assertEquals(1, employees.size) assertEquals(testEmployee, employees.first()) } @After fun tearDown() { database.close() } }

Slide 51

Slide 51 text

@RunWith(RobolectricTestRunner::class) class EmployeeDaoTest { … lateinit var database: ContactsDatabase @Before fun setUp() { database = Room.inMemoryDatabaseBuilder( RuntimeEnvironment.application.baseContext, ContactsDatabase::class.java) .allowMainThreadQueries() .build() } @Test fun shouldInsertAndRetrieveEmployee() { database.teamsDao().insertTeam(testTeam) database.employeesDao().insertEmployee(testEmployee) val employees = database.employeesDao().getEmployeeListByTeamId(TEAM_ID) assertEquals(1, employees.size) assertEquals(testEmployee, employees.first()) } @After fun tearDown() { database.close() } }

Slide 52

Slide 52 text

Migration

Slide 53

Slide 53 text

val MIGRATION_1_2: Migration = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { ... } } val database = Room.databaseBuilder( context.applicationContext, ContactsDatabase::class.java, DATABASE_NAME) .addMigrations(MIGRATION_1_2) .build()

Slide 54

Slide 54 text

val database = Room.databaseBuilder( context.applicationContext, ContactsDatabase::class.java, DATABASE_NAME) .fallbackToDestructiveMigration() .build()

Slide 55

Slide 55 text

val MIGRATION_2_3: Migration = object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { // Create a new table createTable(database, CREATE_EMPLOYEE_TABLE_V3) // Copy the data from the old table to the new one copyData(database, EMPLOYEE_TABLE, TMP_EMPLOYEE_TABLE) // Remove the old table removeTable(database, EMPLOYEE_TABLE) // Rename the new table to the correct name renameTable(database, TMP_EMPLOYEE_TABLE, EMPLOYEE_TABLE) } }

Slide 56

Slide 56 text

object SQLiteTestHelper { fun insertEmployee(database: SupportSQLiteDatabase, teamId: Long, name: String, phone: String) { val values = ContentValues().apply { put(TEAM_ID, teamId) put(EMPLOYEE_NAME, name) put(EMPLOYEE_PHONE, phone) } database.insert(EMPLOYEE, SQLiteDatabase.CONFLICT_REPLACE, values) } fun getMigrationDatabase(helper: MigrationTestHelper): ContactsDatabase { val database = Room.databaseBuilder(InstrumentationRegistry.getTargetContext(), ContactsDatabase::class.java, DatabaseMigrationTests.DATABASE_NAME) .build() helper.closeWhenFinished(database) return database } fun clearDatabase(database: SupportSQLiteDatabase) { database.execSQL("DROP TABLE IF EXISTS $EMPLOYEE") database.execSQL("DROP TABLE IF EXISTS $TEAM") database.close() } }

Slide 57

Slide 57 text

object SQLiteTestHelper { fun insertEmployee(database: SupportSQLiteDatabase, teamId: Long, name: String, phone: String) { val values = ContentValues().apply { put(TEAM_ID, teamId) put(EMPLOYEE_NAME, name) put(EMPLOYEE_PHONE, phone) } database.insert(EMPLOYEE, SQLiteDatabase.CONFLICT_REPLACE, values) } fun getMigrationDatabase(helper: MigrationTestHelper): ContactsDatabase { val database = Room.databaseBuilder( InstrumentationRegistry.getTargetContext(), ContactsDatabase::class.java, DatabaseMigrationTests.DATABASE_NAME) .build() helper.closeWhenFinished(database) return database } fun clearDatabase(database: SupportSQLiteDatabase) { database.execSQL("DROP TABLE IF EXISTS $EMPLOYEE") database.execSQL("DROP TABLE IF EXISTS $TEAM") database.close() } }

Slide 58

Slide 58 text

object SQLiteTestHelper { fun insertEmployee(database: SupportSQLiteDatabase, teamId: Long, name: String, phone: String) { val values = ContentValues().apply { put(TEAM_ID, teamId) put(EMPLOYEE_NAME, name) put(EMPLOYEE_PHONE, phone) } database.insert(EMPLOYEE, SQLiteDatabase.CONFLICT_REPLACE, values) } fun getMigrationDatabase(helper: MigrationTestHelper): ContactsDatabase { val database = Room.databaseBuilder(InstrumentationRegistry.getTargetContext(), ContactsDatabase::class.java, DatabaseMigrationTests.DATABASE_NAME) .build() helper.closeWhenFinished(database) return database } fun clearDatabase(database: SupportSQLiteDatabase) { database.execSQL("DROP TABLE IF EXISTS $EMPLOYEE") database.execSQL("DROP TABLE IF EXISTS $TEAM") database.close() } }

Slide 59

Slide 59 text

@RunWith(AndroidJUnit4::class) class DatabaseMigrationTests { @Rule @JvmField var migrationTestHelper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), ContactsDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()) private lateinit var database: SupportSQLiteDatabase @Before fun setUp() { database = migrationTestHelper.createDatabase(DATABASE_NAME, 1) } @Test fun shouldMigrateDatabaseFromFirstToSecondVersion() { val requiredVersion = 2 val validateDroppedTables = true SQLiteTestHelper.insertTeam(database, TEAM_TITLE) SQLiteTestHelper.insertEmployee(database, TEAM_ID, CONTACT_NAME, CONTACT_PHONE) migrationTestHelper.runMigrationsAndValidate( DATABASE_NAME, requiredVersion, validateDroppedTables, ContactsDatabase.MIGRATION_1_2) val employees = SQLiteTestHelper.getMigrationDatabase(migrationTestHelper).employeesDao().getEmployeeListByTeamId(1) assertEquals(1, employees.size) ... } @After fun tearDown() { SQLiteTestHelper.clearDatabase(database) } }

Slide 60

Slide 60 text

@RunWith(AndroidJUnit4::class) class DatabaseMigrationTests { @Rule @JvmField var migrationTestHelper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), ContactsDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()) private lateinit var database: SupportSQLiteDatabase @Before fun setUp() { database = migrationTestHelper.createDatabase(DATABASE_NAME, 1) } @Test fun shouldMigrateDatabaseFromFirstToSecondVersion() { val requiredVersion = 2 val validateDroppedTables = true SQLiteTestHelper.insertTeam(database, TEAM_TITLE) SQLiteTestHelper.insertEmployee(database, TEAM_ID, CONTACT_NAME, CONTACT_PHONE) migrationTestHelper.runMigrationsAndValidate( DATABASE_NAME, requiredVersion, validateDroppedTables, ContactsDatabase.MIGRATION_1_2) val employees = SQLiteTestHelper.getMigrationDatabase(migrationTestHelper) .employeesDao() .getEmployeeListByTeamId(1) assertEquals(1, employees.size) ... } @After fun tearDown() { SQLiteTestHelper.clearDatabase(database) } }

Slide 61

Slide 61 text

@RunWith(AndroidJUnit4::class) class DatabaseMigrationTests { @Rule @JvmField var migrationTestHelper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), ContactsDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()) private lateinit var database: SupportSQLiteDatabase @Before fun setUp() { database = migrationTestHelper.createDatabase(DATABASE_NAME, 1) } @Test fun shouldMigrateDatabaseFromFirstToSecondVersion() { val requiredVersion = 2 val validateDroppedTables = true SQLiteTestHelper.insertTeam(database, TEAM_TITLE) SQLiteTestHelper.insertEmployee(database, TEAM_ID, CONTACT_NAME, CONTACT_PHONE) migrationTestHelper.runMigrationsAndValidate(DATABASE_NAME, requiredVersion, validateDroppedTables, ContactsDatabase.MIGRATION_1_2) val employees = SQLiteTestHelper.getMigrationDatabase(migrationTestHelper) .employeesDao() .getEmployeeListByTeamId(1) assertEquals(1, employees.size) ... } @After fun tearDown() { SQLiteTestHelper.clearDatabase(database) } }

Slide 62

Slide 62 text

@RunWith(AndroidJUnit4::class) class DatabaseMigrationTests { @Rule @JvmField var migrationTestHelper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), ContactsDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()) private lateinit var database: SupportSQLiteDatabase @Before fun setUp() { database = migrationTestHelper.createDatabase(DATABASE_NAME, 1) } @Test fun shouldMigrateDatabaseFromFirstToSecondVersion() { val requiredVersion = 2 val validateDroppedTables = true SQLiteTestHelper.insertTeam(database, TEAM_TITLE) SQLiteTestHelper.insertEmployee(database, TEAM_ID, CONTACT_NAME, CONTACT_PHONE) migrationTestHelper.runMigrationsAndValidate( DATABASE_NAME, requiredVersion, validateDroppedTables, ContactsDatabase.MIGRATION_1_2) val employees = SQLiteTestHelper.getMigrationDatabase(migrationTestHelper) .employeesDao() .getEmployeeListByTeamId(1) assertEquals(1, employees.size) ... } @After fun tearDown() { SQLiteTestHelper.clearDatabase(database) } }

Slide 63

Slide 63 text

LiveData RxJava

Slide 64

Slide 64 text

@Query("SELECT * FROM employees") fun getEmployees(): LiveData>

Slide 65

Slide 65 text

@Query("SELECT * FROM employees") fun getEmployees(): Flowable>

Slide 66

Slide 66 text

@Query("SELECT * FROM employees") fun getEmployees(): Single>

Slide 67

Slide 67 text

@Query("SELECT * FROM employees") fun getEmployees(): Maybe>

Slide 68

Slide 68 text

Q&A Room Persistence Library https://developer.android.com/topic/libraries/architecture/room.html 7 Steps To Room https://medium.com/google-developers/7-steps-to-room-27a5fe5f99b2 7 Pro-tips for Room https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1 Blog http://alexzh.com/ @Alex_Zhukovich