Using Realm persistence library in a Kotlin Multiplatform project

Share on:

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).



As a point of reference this is what the UI looks like for our iOS and Android apps Screenshots


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 Flows 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