Skip to content

Commit

Permalink
Migrate from Room to SQLDelight
Browse files Browse the repository at this point in the history
  • Loading branch information
fflopsi committed Mar 31, 2024
1 parent 64b0c63 commit 70ea2fc
Show file tree
Hide file tree
Showing 17 changed files with 269 additions and 328 deletions.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ plugins {
id("org.jetbrains.kotlin.android") apply false
id("org.jetbrains.compose") apply false
id("androidx.room") version "2.6.1" apply false
id("app.cash.sqldelight") version "2.0.1" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.22" apply false
kotlin("kapt") version "1.9.22" apply false
}
20 changes: 15 additions & 5 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ plugins {
id("com.android.library")
id("kotlin-parcelize")
id("androidx.room")
id("app.cash.sqldelight")
id("org.jetbrains.kotlin.plugin.serialization")
kotlin("kapt")
}

kotlin {
Expand All @@ -25,6 +27,8 @@ kotlin {
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.arkivanov.decompose:decompose:$decomposeVersion")
implementation("com.arkivanov.decompose:extensions-compose:$decomposeVersion")

implementation("app.cash.sqldelight:coroutines-extensions:2.0.1")
}
}
androidMain {
Expand All @@ -40,11 +44,7 @@ kotlin {
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("androidx.navigation:navigation-compose:2.7.7")

val roomVersion = "2.6.1"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
//annotationProcessor("androidx.room:room-compiler:$roomVersion")
//configurations["kapt"].dependencies.add(project.dependencies.create("androidx.room:room-compiler:$roomVersion"))
implementation("app.cash.sqldelight:android-driver:2.0.1")
}
}
jvmMain {
Expand All @@ -54,6 +54,8 @@ kotlin {
val multiplatformSettingsVersion = "1.1.1"
implementation("com.russhwolf:multiplatform-settings:$multiplatformSettingsVersion")
implementation("com.russhwolf:multiplatform-settings-coroutines:$multiplatformSettingsVersion")

implementation("app.cash.sqldelight:sqlite-driver:2.0.1")
}
}
}
Expand All @@ -76,3 +78,11 @@ android {
schemaDirectory("$projectDir/schemas/")
}
}

sqldelight {
databases {
create("TournamentsDB") {
packageName.set("me.frauenfelderflorian.tournamentscompose")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@ import androidx.compose.ui.platform.LocalContext
import androidx.core.view.WindowCompat
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.room.Room
import com.arkivanov.decompose.defaultComponentContext
import com.arkivanov.decompose.router.stack.StackNavigation
import kotlinx.coroutines.launch
import me.frauenfelderflorian.tournamentscompose.common.data.DriverFactory
import me.frauenfelderflorian.tournamentscompose.common.data.GameDao
import me.frauenfelderflorian.tournamentscompose.common.data.PlayersModel
import me.frauenfelderflorian.tournamentscompose.common.data.Prefs
import me.frauenfelderflorian.tournamentscompose.common.data.PrefsFactory
import me.frauenfelderflorian.tournamentscompose.common.data.TournamentsDatabase
import me.frauenfelderflorian.tournamentscompose.common.data.TournamentDao
import me.frauenfelderflorian.tournamentscompose.common.data.TournamentWithGames
import me.frauenfelderflorian.tournamentscompose.common.data.TournamentsModel
import me.frauenfelderflorian.tournamentscompose.common.data.createDatabase
import me.frauenfelderflorian.tournamentscompose.common.ui.ProvideComponentContext
import me.frauenfelderflorian.tournamentscompose.common.ui.Screen
import me.frauenfelderflorian.tournamentscompose.common.ui.importFromUri
Expand All @@ -55,12 +58,19 @@ fun androidApp(activity: ComponentActivity) {
fun AndroidAppContent(intent: Intent) {
val context = LocalContext.current
val prefs: Prefs = viewModel<Prefs>(factory = PrefsFactory(context)).apply { Initialize() }
val db = Room.databaseBuilder(context, TournamentsDatabase::class.java, "tournaments").build()
val tournamentDao = db.tournamentDao()
val gameDao = db.gameDao()
val database = createDatabase(DriverFactory(context))
val tournamentDao = TournamentDao(database.tournamentQueries)
val gameDao = GameDao(database.gameQueries)
val model: TournamentsModel = viewModel()
model.tournaments = tournamentDao.getTournamentsWithGames().asLiveData()
.observeAsState(model.tournaments.values).value.associateBy { it.t.id }
tournamentDao.getTournaments().asLiveData().observeAsState(listOf()).value.associateBy(
keySelector = { it.id },
valueTransform = {
TournamentWithGames(
t = it,
games = gameDao.getGames(it.id).asLiveData().observeAsState(listOf()).value
)
},
).also { model.tournaments = it }
val playersModel: PlayersModel = viewModel()
val navigator = remember { StackNavigation<Screen>() }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,12 @@
package me.frauenfelderflorian.tournamentscompose.common.data

import androidx.room.Dao
import androidx.room.Database
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.RoomDatabase
import androidx.room.Transaction
import androidx.room.Update
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow

@Dao
actual interface TournamentDao {
@Transaction
@Query("SELECT * FROM tournament")
actual fun getTournamentsWithGames(): Flow<List<TournamentWithGames>>

@Insert
actual suspend fun insert(vararg tournaments: Tournament)

@Update
actual suspend fun update(vararg tournaments: Tournament)

@Upsert
actual suspend fun upsert(vararg tournaments: Tournament)

@Delete
actual suspend fun delete(tournament: Tournament)
}

@Dao
actual interface GameDao {
@Insert
actual suspend fun insert(vararg games: Game)

@Update
actual suspend fun update(vararg games: Game)

@Upsert
actual suspend fun upsert(vararg games: Game)

@Delete
actual suspend fun delete(game: Game)
}

@Database(entities = [Tournament::class, Game::class], version = 1)
actual abstract class TournamentsDatabase : RoomDatabase() {
actual abstract fun tournamentDao(): TournamentDao
actual abstract fun gameDao(): GameDao
import android.content.Context
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import me.frauenfelderflorian.tournamentscompose.TournamentsDB

actual class DriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(TournamentsDB.Schema, context, "TournamentsDB")
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
package me.frauenfelderflorian.tournamentscompose.common.data

import androidx.lifecycle.ViewModel
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import androidx.room.Relation
import java.util.UUID

actual class TournamentsModel : ViewModel() {
Expand All @@ -17,43 +12,3 @@ actual class PlayersModel : ViewModel() {
actual val players = mutableListOf<String>()
actual var edited = false
}

actual data class TournamentWithGames actual constructor(
@Embedded actual val t: Tournament,
@Relation(parentColumn = "id", entityColumn = "tournamentId") actual val games: List<Game>,
) {
@Ignore
actual var current: Game? = null
}

@Entity
actual data class Tournament actual constructor(
@PrimaryKey actual val id: UUID,
actual val name: String,
actual val start: Long,
actual val end: Long,
actual val useAdaptivePoints: Boolean,
actual val firstPoints: Int,
/**
* Concatenated list of all players in this tournament. Use [players] to access.
*
* Leave this empty upon instantiation, and modify it afterwards using [players]
*/
actual var playersString: String,
)

@Entity
actual data class Game actual constructor(
@PrimaryKey actual val id: UUID,
actual val tournamentId: UUID,
actual val date: Long,
actual val hoops: Int,
actual val hoopReached: Int,
actual val difficulty: String,
/**
* JSON version of the map of the ranking of this game. Use [ranking] to access
*
* Leave this empty upon instantiation, and modify it afterwards using [ranking]
*/
actual var rankingString: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ import kotlinx.coroutines.withContext
import me.frauenfelderflorian.tournamentscompose.common.data.GameDao
import me.frauenfelderflorian.tournamentscompose.common.data.TournamentDao
import me.frauenfelderflorian.tournamentscompose.common.data.TournamentWithGames
import me.frauenfelderflorian.tournamentscompose.common.data.players
import me.frauenfelderflorian.tournamentscompose.common.data.ranking
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.stringResource
import tournamentscompose.common.generated.resources.*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,99 @@
package me.frauenfelderflorian.tournamentscompose.common.data

import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import app.cash.sqldelight.db.SqlDriver
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import me.frauenfelderflorian.tournamentscompose.GameQueries
import me.frauenfelderflorian.tournamentscompose.TournamentQueries
import me.frauenfelderflorian.tournamentscompose.TournamentsDB
import java.nio.ByteBuffer
import java.util.UUID

expect interface TournamentDao {
fun getTournamentsWithGames(): Flow<List<TournamentWithGames>>
suspend fun insert(vararg tournaments: Tournament)
suspend fun update(vararg tournaments: Tournament)
suspend fun upsert(vararg tournaments: Tournament)
suspend fun delete(tournament: Tournament)
expect class DriverFactory {
fun createDriver(): SqlDriver
}

expect interface GameDao {
suspend fun insert(vararg games: Game)
suspend fun update(vararg games: Game)
suspend fun upsert(vararg games: Game)
suspend fun delete(game: Game)
fun createDatabase(driverFactory: DriverFactory): TournamentsDB {
return TournamentsDB(driverFactory.createDriver())
}

expect abstract class TournamentsDatabase {
abstract fun tournamentDao(): TournamentDao
abstract fun gameDao(): GameDao
class TournamentDao(private val queries: TournamentQueries) {
fun getTournaments(): Flow<List<Tournament>> {
return queries.getTournaments { id, name, start, end, useAdaptivePoints, firstPoints, playersString ->
Tournament(
id = id.asUuid(),
name = name,
start = start,
end = end,
useAdaptivePoints = useAdaptivePoints.toInt() == 1,
firstPoints = firstPoints.toInt(),
playersString = playersString
)
}.asFlow().mapToList(Dispatchers.IO)
}

fun upsert(vararg tournaments: Tournament) {
tournaments.forEach {
queries.upsert(
name = it.name,
start = it.start,
end = it.end,
useAdaptivePoints = it.useAdaptivePoints.toInt().toLong(),
firstPoints = it.firstPoints.toLong(),
playersString = it.playersString,
id = it.id.asByteArray()
)
}
}

fun delete(tournament: Tournament) {
queries.delete(tournament.id.asByteArray())
}
}

class GameDao(private val queries: GameQueries) {
fun getGames(tournamentId: UUID): Flow<List<Game>> {
return queries.getTournamentGames(
tournamentId.asByteArray()
) { id, tId, date, hoops, hoopReached, difficulty, rankingString ->
Game(
id = id.asUuid(),
tournamentId = tId.asUuid(),
date = date,
hoops = hoops.toInt(),
hoopReached = hoopReached.toInt(),
difficulty = difficulty,
rankingString = rankingString
)
}.asFlow().mapToList(Dispatchers.IO)
}

fun upsert(vararg games: Game) {
games.forEach {
queries.upsert(
date = it.date,
hoops = it.hoops.toLong(),
hoopReached = it.hoopReached.toLong(),
difficulty = it.difficulty,
rankingString = it.rankingString,
id = it.id.asByteArray(),
tournamentId = it.tournamentId.asByteArray()
)
}
}

fun delete(game: Game) {
queries.delete(game.id.asByteArray())
}
}

private fun UUID.asByteArray(): ByteArray =
ByteBuffer.wrap(ByteArray(16)).putLong(this.mostSignificantBits)
.putLong(this.leastSignificantBits).array()

private fun ByteArray.asUuid(): UUID =
UUID(ByteBuffer.wrap(this).getLong(0), ByteBuffer.wrap(this).getLong(8))

private fun Boolean.toInt(): Int = if (this) 1 else 0
Loading

0 comments on commit 70ea2fc

Please sign in to comment.