It’s always great to see the Kotlin Multiplatform (KMP) library ecosystem continuing to grow and a relatively new addition to that is
Realm which describes itself as “a mobile database that runs directly inside phones, tablets or wearables.” and “a replacement for SQLite & ORMs”.
An added positive is fact that it’s another example of existence now of multiple different KMP libraries addressing particular functionality…with for
example SQLDelight, Kodein-DB, and, somewhat indirectly,
Apollo GraphQL libraries providing persistence capabilities (previous articles
here have described integrating those). Realm KMP library is in developer preview right now, and is still undergoing active development, but I thought it would be
interesting to explore adding it as persistence layer for a KMP sample, FantasyPremierLeague,
that I created recently (note that the changes shown here have been pushed to the realm
branch of that repo).
Check out the story of Realm's Kotlin Multiplatform SDK!
— Kotlin (@kotlin) April 20, 2021
Learn what motivated their Engineering team’s decision to build a new SDK and the key design principles that guided their development.https://t.co/EnKFGoqVLj
As a point of reference this is what the UI looks like for our iOS and Android apps
Setup
To use Realm in the project we firstly added following to root build.gradle.kts
repositories:
maven(url = "http://oss.jfrog.org/artifactory/oss-snapshot-local")
dependencies:
classpath("io.realm.kotlin:gradle-plugin:0.0.1-SNAPSHOT")
And, in build.gradle.kts
for the KMP shared code module, we add:
plugins:
id("realm-kotlin")
commonMain dependencies:
implementation(io.realm.kotlin:library:0.0.1-SNAPSHOT)
Shared code updates
The FantasyPremierLeague repo includes a class, FantasyPremierLeagueRepository
, which exposes
API from shared code for accessing for example list of players and game fixtures. In initial implementation there was no persistence and each time
these methods were called they’d make remote API requests (using FantasyPremierLeagueApi
which in turn makes use of Ktor
library).
To start using Realm we firstly need to define our data model as shown below. Note here that both PlayerDb
and FixtureDb
refer to TeamnDb
.
class TeamDb: RealmObject {
var id: Int = 0
var index: Int = 0
var name: String = ""
var code: Int = 0
}
class PlayerDb: RealmObject {
var id: Int = 0
var firstName: String = ""
var secondName: String = ""
var code: Int = 0
var teamCode: Int = 0
var totalPoints: Int = 0
var nowCost: Int = 0
var goalsScored: Int = 0
var assists: Int = 0
var team: TeamDb? = null
}
class FixtureDb: RealmObject {
var id: Int = 0
var kickoffTime: String? = ""
var homeTeam: TeamDb? = null
var awayTeam: TeamDb? = null
var homeTeamScore: Int = 0
var awayTeamScore: Int = 0
}
Next up we create our Realm
instance
private val realm: Realm by lazy {
val configuration = RealmConfiguration(schema = setOf(PlayerDb::class, TeamDb::class, FixtureDb::class))
Realm.open(configuration)
}
Now we can start using this to write data to the database and also to observe database changes which we’ll ultimately now use to drive updates to the UI. In our initial basic implementation we read data from remote api endpoint and store to database on startup using following.
private suspend fun loadData() {
val bootstrapStaticInfoDto = fantasyPremierLeagueApi.fetchBootstrapStaticInfo()
val fixtures = fantasyPremierLeagueApi.fetchFixtures()
realm.writeBlocking {
// basic implementation for now where we recreate/repopulate db on startup
realm.objects<TeamDb>().delete()
realm.objects<PlayerDb>().delete()
realm.objects<FixtureDb>().delete()
// store teams
bootstrapStaticInfoDto.teams.forEachIndexed { teamIndex, teamDto ->
copyToRealm(TeamDb().apply {
id = teamDto.id
index = teamIndex + 1
name = teamDto.name
code = teamDto.code
})
}
// store players
bootstrapStaticInfoDto.elements.forEach { player ->
copyToRealm(PlayerDb().apply {
id = player.id
firstName = player.first_name
secondName = player.second_name
code = player.code
teamCode = player.team_code
totalPoints = player.total_points
nowCost = player.now_cost
goalsScored = player.goals_scored
assists = player.assists
team = realm.objects<TeamDb>().query("code = $0", player.team_code).first()
})
}
// store fixtures
val teams = realm.objects<TeamDb>().toList()
fixtures.forEach { fixtureDto ->
if (fixtureDto.kickoff_time != null) {
copyToRealm(FixtureDb().apply {
id = fixtureDto.id
kickoffTime = fixtureDto.kickoff_time
fixtureDto.team_h_score?.let { homeTeamScore = it }
fixtureDto.team_a_score?.let { awayTeamScore = it }
homeTeam = teams.find { it.index == fixtureDto.team_h }
awayTeam = teams.find { it.index == fixtureDto.team_a }
})
}
}
}
}
Now we can observe updates to the database using following code. Any changes are mapped to the data model classes
that we’re exposing from our repository (e.g. Player
and GameFixture
) and stored in StateFlow
variables.
realm.objects(TeamDb::class).observe {
_teamList.value = it.toList().map {
Team(it.id, it.index, it.name, it.code)
}
}
realm.objects(PlayerDb::class).observe {
_playerList.value = it.toList().map {
val playerName = "${it.firstName} ${it.secondName}"
val playerImageUrl = "https://resources.premierleague.com/premierleague/photos/players/110x140/p${it.code}.png"
val teamName = it.team?.name ?: ""
val currentPrice = it.nowCost / 10.0
Player(it.id, playerName, teamName, playerImageUrl, it.totalPoints, currentPrice, it.goalsScored, it.assists)
}
}
realm.objects(FixtureDb::class).observe {
_fixtureList.value = it.toList().map {
val homeTeamName = it.homeTeam?.name ?: ""
val homeTeamCode = it.homeTeam?.code ?: 0
val homeTeamScore = it.homeTeamScore ?: 0
val homeTeamPhotoUrl = "https://resources.premierleague.com/premierleague/badges/t${homeTeamCode}.png"
val awayTeamName = it.awayTeam?.name ?: ""
val awayTeamCode = it.awayTeam?.code ?: 0
val awayTeamScore = it.awayTeamScore ?: 0
val awayTeamPhotoUrl = "https://resources.premierleague.com/premierleague/badges/t${awayTeamCode}.png"
it.kickoffTime?.let { kickoffTime ->
val localKickoffTime = kickoffTime.toInstant().toLocalDateTime(TimeZone.currentSystemDefault())
GameFixture(it.id, localKickoffTime, homeTeamName, awayTeamName,
homeTeamPhotoUrl, awayTeamPhotoUrl, homeTeamScore, awayTeamScore)
}
}.filterNotNull()
}
Android related updates
In our Android ViewModel we can reference the StateFlow
variables exposed from our repository (in this example we’re combining with
possible search term for a player)
val searchQuery = MutableStateFlow("")
val playerList: StateFlow<List<Player>> = searchQuery.debounce(250).flatMapLatest { searchQuery ->
repository.playerList.mapLatest { playerList ->
playerList
.filter { it.name.contains(searchQuery, ignoreCase = true) }
.sortedByDescending { it.points }
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
And then in our Jetpack Compose code we can finally use this to show list of players in UI.
val playerList = fantasyPremierLeagueViewModel.playerList.collectAsState()
LazyColumn {
items(items = playerList.value, itemContent = { player ->
PlayerView(player, onPlayerSelected)
})
}
iOS related updates
There are a number of different approaches that can be used to expose Flow
s from shared code to iOS clients but in this case
we’re going to just pass lambda/closure from our Swift ViewModel to following method in FantasyPremierLeagueRepository
// called from iOS
fun getPlayers(success: (List<Player>) -> Unit) {
mainScope.launch {
playerList.collect {
success(it.sortedByDescending { it.points })
}
}
}
This is then what our iOS Swift ViewModel looks like
class FantasyPremierLeagueViewModel: ObservableObject {
@Published var playerList = [Player]()
@Published var fixtureList = [GameFixture]()
private let repository: FantasyPremierLeagueRepository
init(repository: FantasyPremierLeagueRepository) {
self.repository = repository
}
func getPlayers() {
repository.getPlayers { playerList in
self.playerList = playerList
}
}
func getFixtures() {
repository.getFixtures { fixtureList in
self.fixtureList = fixtureList
}
}
}
And finally this is part of SwiftUI code used to render the list of players.
List(viewModel.playerList, id: \.id) { player in
NavigationLink(destination: PlayerDetailsView(player: player)) {
PlayerView(player: player)
}
}
.onAppear {
viewModel.getPlayers()
}
Featured in Kotlin Weekly Issue #249 and Android Weekly Issue #465
Related tweet
Got chance this evening to make some small final updates to article about trying out @realm's new Kotlin Multiplatform library in FantasyPremierLeague repo (https://t.co/4Bh5TzZM60)https://t.co/GllhF7su13 https://t.co/FICGkxQHX5
— John O'Reilly (@joreilly) May 6, 2021