Google and JetBrains have been combining to make many of the previously Android only Jetpack libraries work in Kotlin Multiplatform (KMP) code. We had seen releases of Lifecycle, ViewModel, DataStore and Navigation and as of earlier this week Room is also now available. Exciting times! In this article I’m going to outline steps taken to add use of Room to the FantasyPremierLeague KMP sample. This project had already been using Jetpack ViewModel and DataStore libraries.


Project overview

FantasyPremierLeague is a Kotlin Multiplatform (KMP) sample for showing information for a compeiition based on the Premier League (the main football league in England), The following are screenshots of the Android and iOS clients.

Database Inspector


Changes made

We’re going to show here the changes that were made in the KMP shared code to add Room support. The data in this case is returned from the backend using Ktor.

Entities

The following are the entities we’re dealing with in this project - Teams, Players and Fixtures. Note you can auto generate primary keys but we’re using ids from the backend data in this case.

Team

1
2
3
4
5
6
7
@Entity
data class Team(
    @PrimaryKey val id: Int,
    val index: Int,
    val name: String,
    val code: Int
)

Player

1
2
3
4
5
6
7
8
9
10
11
@Entity
data class Player(
    @PrimaryKey val id: Int,
    val name: String,
    val team: String,
    val photoUrl: String,
    val points: Int,
    val currentPrice: Double,
    val goalsScored: Int,
    val assists: Int
)

Fixture

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
data class GameFixture(
    @PrimaryKey val id: Int,
    val localKickoffTime: LocalDateTime,
    val homeTeam: String,
    val awayTeam: String,
    val homeTeamPhotoUrl: String,
    val awayTeamPhotoUrl: String,
    val homeTeamScore: Int?,
    val awayTeamScore: Int?,
    val event: Int
  )


Database class

We create the AppDatabase abstract class as shown below in common code. The actual implementation is created for each platform as we’ll see shortly. We also tell it the entties that we’re going to be using (a SQLite database table will be created for each of these).

1
2
3
4
5
6
7
@Database(entities = [Team::class, Player::class, GameFixture::class], version = 1)
@TypeConverters(LocalDateTimeConverter::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun fantasyPremierLeagueDao(): FantasyPremierLeagueDao
}

internal const val dbFileName = "fantasypremierleague.db"

Note that the GameFixture entity makes use of LocalDateTime and we create the following type converter for that (configured using @TypeConverters above)

1
2
3
4
5
6
7
8
9
10
11
class LocalDateTimeConverter {
    @TypeConverter
    fun fromTimestamp(value: String?): LocalDateTime? {
        return value?.let { LocalDateTime.parse(it) }
    }

    @TypeConverter
    fun dateToTimestamp(date: LocalDateTime?): String? {
        return date?.toString()
    }
}


Database Builder

The project uses the Koin DI framework and in turn uses KMP’s expect/actual mechanism to create per platform Koin modules and that is where we create the AppDatabase implementation for each platform.

Android

1
2
3
4
5
6
7
fun createRoomDatabase(ctx: Context): AppDatabase {
    val dbFile = ctx.getDatabasePath(dbFileName)
    return Room.databaseBuilder<AppDatabase>(ctx, dbFile.absolutePath)
        .setDriver(BundledSQLiteDriver())
        .setQueryCoroutineContext(Dispatchers.IO)
        .build()
}

iOS

1
2
3
4
5
6
7
8
9
fun createRoomDatabase(): AppDatabase {
    val dbFile = "${fileDirectory()}/$dbFileName"
    return Room.databaseBuilder<AppDatabase>(
        name = dbFile,
        factory =  { AppDatabase::class.instantiateImpl() }
    ).setDriver(BundledSQLiteDriver())
        .setQueryCoroutineContext(Dispatchers.IO)
        .build()
}


DAO class

The following is the DAO class containing the queries we’ll be making.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Dao
interface FantasyPremierLeagueDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertTeamList(teamList: List<Team>)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertPlayerList(playerList: List<Player>)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertFixtureList(fixtureList: List<GameFixture>)

    @Query("SELECT * FROM Player")
    fun getPlayerListAsFlow(): Flow<List<Player>>

    @Query("SELECT * FROM Player WHERE id = :id")
    suspend fun getPlayer(id: Int): Player

    @Query("SELECT * FROM GameFixture")
    fun getFixtureListAsFlow(): Flow<List<GameFixture>>

    @Query("SELECT * FROM GameFixture WHERE id = :id")
    suspend fun getFixture(id: Int): GameFixture
}


Repository

This is code from the repository class that makes use of that DAO to interact with the database. We firstly show here how we’re using the DAO to store the data we got from the backend in to the database.

1
2
3
database.fantasyPremierLeagueDao().insertTeamList(teamList)
database.fantasyPremierLeagueDao().insertPlayerList(playerList)
database.fantasyPremierLeagueDao().insertFixtureList(fixtureList)

And then using it to expose the data as flows which are accessed then in our view models. It also contains methods to access particular entities.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun getPlayers(): Flow<List<Player>> {
    return database.fantasyPremierLeagueDao().getPlayerListAsFlow()
}

suspend fun getPlayer(id: Int): Player {
    return database.fantasyPremierLeagueDao().getPlayer(id)
}

fun getFixtures(): Flow<List<GameFixture>> {
    return database.fantasyPremierLeagueDao().getFixtureListAsFlow()
}

suspend fun getFixture(id: Int): GameFixture {
    return database.fantasyPremierLeagueDao().getFixture(id)
}


Dependencies

The following are the changes made to the project to add/use Room related dependencies.

libs.version.toml

1
2
3
4
5
6
7
8
9
10
11
[versions]
androidxRoom = "2.7.0-alpha01"
sqlite = "2.5.0-SNAPSHOT"

[libraries]
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidxRoom" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidxRoom" }
sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }

[plugins]
room = { id = "androidx.room", version.ref = "androidxRoom" }

build.gradle.kts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
plugins {
    ...
    alias(libs.plugins.room)
}

...

commonMain.dependencies {
    ...
    implementation(libs.androidx.room.runtime)
    implementation(libs.sqlite.bundled)
}

...

dependencies {
    add("kspAndroid", libs.androidx.room.compiler)
    add("kspIosSimulatorArm64", libs.androidx.room.compiler)
    add("kspIosX64", libs.androidx.room.compiler)
    add("kspIosArm64", libs.androidx.room.compiler)
}

room {
    schemaDirectory("$projectDir/schemas")
}


The database created can also be viewed/queried using Android Studio’s Database Inspector as shown below.

Database Inspector


Featured in Kotlin Weekly Issue #405 and Android Weekly #621