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.
To recap, following AndroidX/Jetpack libraries can now be used in Kotlin Multiplatform code! #KMP
— John O'Reilly (@joreilly) May 1, 2024
🚀 Lifecycle https://t.co/p6N0Hklhgg
🚀 ViewModel https://t.co/TdGTu7zU1O
🚀 Navigation https://t.co/Uz36dZ4wEw
🚀 DataStore https://t.co/dkSHAHJelH
🚀 Room https://t.co/eCdgxBaDXD
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.
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.
Featured in Kotlin Weekly Issue #405 and Android Weekly #621
Related tweet
Using Jetpack Room in Kotlin Multiplatform shared code https://t.co/MWA5SuutOo
— John O'Reilly (@joreilly) May 3, 2024
Wrote short article about steps taken to add Room to the FantasyPremierLeague KMP sample.