Kotlin and Android Development featuring Jetpack: Unique constraint error when running chapter 5 code changes

@mfazio23

I’ve applied the changes from Chapter 5 of the book and everything builds correctly and runs. But, when I try to start a game, the following exception is being thrown:

android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: game_statuses.gameId, game_statuses.playerId (code 1555 SQLITE_CONSTRAINT_PRIMARYKEY)
    at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
    at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:940)
    at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:790)
    at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:89)
    at androidx.sqlite.db.framework.FrameworkSQLiteStatement.executeInsert(FrameworkSQLiteStatement.kt:42)
    at androidx.room.EntityInsertionAdapter.insert(EntityInsertionAdapter.kt:85)
    at dev.mfazio.pennydrop.data.PennyDropDao_Impl$10.call(PennyDropDao_Impl.java:278)
    at dev.mfazio.pennydrop.data.PennyDropDao_Impl$10.call(PennyDropDao_Impl.java:273)
   <snipped>

Any suggestion on what to check in the project code?

Sorry for the delay here, work’s been rough lately.

For this issue, can you see which values are being saved? It seems like you’re trying to add duplicate players to the same game, which is why it’s complaining about unique primary keys.

I think I remember this happening if you try to add multiple of the same bot to a game.

I know the feeling: work and holiday travel make for less fun coding. :smiley:

I’m having a heck of a time starting up the app: it’s still crashing on startup. But if I clear out the data and start it fresh, I do see the set of AI players area present in the database. So it feels like it’s trying to redo the initial database load whenever I start the app, rather than performing a check first.

That’s possible, which would explain the stack trace you posted before.

Can you share your database creation code? I wonder if you overrode the onOpen function rather than onCreate.

Here’s the full source for the PennyDropDatabase class in my repo:

package dev.mfazio.pennydrop.data

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.sqlite.db.SupportSQLiteDatabase
import dev.mfazio.pennydrop.game.AI
import dev.mfazio.pennydrop.types.Player
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

@Database(
    entities = [Game::class, Player::class, GameStatus::class],
    version = 1,
    exportSchema = false
)
@TypeConverters(Converters::class)
abstract class PennyDropDatabase : RoomDatabase() {
    abstract fun pennyDropDao(): PennyDropDao

    companion object {
        @Volatile
        private var instance: PennyDropDatabase? = null

        fun getDatabase(context: Context, scope: CoroutineScope): PennyDropDatabase =
            instance ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context,
                    PennyDropDatabase::class.java,
                    "PennyDropDatabase"
                ).addCallback(object : Callback() {
                    override fun onCreate(db: SupportSQLiteDatabase) {
                        super.onCreate(db)
                        scope.launch {
                            instance?.pennyDropDao()?.insertPlayers(
                                AI.basicAI.map(AI::toPlayer)
                            )
                        }
                    }
                })
                    .build()
                this.instance = instance

                instance
            }
    }
}

The code there looks fine and I can see that your repo is at least starting correctly. What’s the stack trace from the start up crash?

I’m seeing this:

2023-07-09 15:57:42.833  6559-6559  AndroidRuntime          dev.mfazio.pennydrop                 E  FATAL EXCEPTION: main
                                                                                                    Process: dev.mfazio.pennydrop, PID: 6559
                                                                                                    android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: game_statuses.gameId, game_statuses.playerId (code 1555 SQLITE_CONSTRAINT_PRIMARYKEY)
                                                                                                    	at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
                                                                                                    	at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:940)
                                                                                                    	at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:790)
                                                                                                    	at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:89)
                                                                                                    	at androidx.sqlite.db.framework.FrameworkSQLiteStatement.executeInsert(FrameworkSQLiteStatement.kt:42)
                                                                                                    	at androidx.room.EntityInsertionAdapter.insert(EntityInsertionAdapter.kt:85)
                                                                                                    	at dev.mfazio.pennydrop.data.PennyDropDao_Impl$10.call(PennyDropDao_Impl.java:278)
                                                                                                    	at dev.mfazio.pennydrop.data.PennyDropDao_Impl$10.call(PennyDropDao_Impl.java:273)
                                                                                                    	at androidx.room.CoroutinesRoom$Companion.execute(CoroutinesRoom.kt:57)
                                                                                                    	at androidx.room.CoroutinesRoom.execute(Unknown Source:2)
                                                                                                    	at dev.mfazio.pennydrop.data.PennyDropDao_Impl.insertGameStatuses(PennyDropDao_Impl.java:273)
                                                                                                    	at dev.mfazio.pennydrop.data.PennyDropDao.startGame$suspendImpl(PennyDropDao.kt:75)
                                                                                                    	at dev.mfazio.pennydrop.data.PennyDropDao.startGame(Unknown Source:0)
                                                                                                    	at dev.mfazio.pennydrop.data.PennyDropDao_Impl.access$1001(PennyDropDao_Impl.java:37)
                                                                                                    	at dev.mfazio.pennydrop.data.PennyDropDao_Impl.lambda$startGame$0$dev-mfazio-pennydrop-data-PennyDropDao_Impl(PennyDropDao_Impl.java:326)
                                                                                                    	at dev.mfazio.pennydrop.data.PennyDropDao_Impl$$ExternalSyntheticLambda1.invoke(Unknown Source:6)
                                                                                                    	at androidx.room.RoomDatabaseKt$withTransaction$transactionBlock$1.invokeSuspend(RoomDatabaseExt.kt:56)
                                                                                                    	at androidx.room.RoomDatabaseKt$withTransaction$transactionBlock$1.invoke(Unknown Source:8)
                                                                                                    	at androidx.room.RoomDatabaseKt$withTransaction$transactionBlock$1.invoke(Unknown Source:4)
                                                                                                    	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
                                                                                                    	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:169)
                                                                                                    	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
                                                                                                    	at androidx.room.RoomDatabaseKt$startTransactionCoroutine$2$1$1.invokeSuspend(RoomDatabaseExt.kt:97)
                                                                                                    	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
                                                                                                    	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
                                                                                                    	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:284)
                                                                                                    	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
                                                                                                    	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
                                                                                                    	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
                                                                                                    	at androidx.room.RoomDatabaseKt$startTransactionCoroutine$2$1.run(RoomDatabaseExt.kt:93)
                                                                                                    	at androidx.room.TransactionExecutor.execute$lambda$1$lambda$0(TransactionExecutor.kt:36)
                                                                                                    	at androidx.room.TransactionExecutor.$r8$lambda$AympDHYBb78s7_N_9gRsXF0sHiw(Unknown Source:0)
                                                                                                    	at androidx.room.TransactionExecutor$$ExternalSyntheticLambda0.run(Unknown Source:4)
                                                                                                    	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
                                                                                                    	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
                                                                                                    	at java.lang.Thread.run(Thread.java:920)
                                                                                                    	Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@e8d038d, Dispatchers.Main.immediate]
2023-07-09 15:57:43.622  6559-6587  EGL_emulation           dev.mfazio.pennydrop                 D  app_time_stats: avg=18.40ms min=2.49ms max=59.10ms count=50

That’s really weird since it shouldn’t try to start a game on app startup, but when you click the start game button.

The main reason that’ll be thrown is if you have empty spots in your game list or the same bot in two spots. Since it’s the same user and same game, it throws that exception. Is that what’s happening for you?

You’re not there yet, but I mention this bug at the end of chapter 7. I didn’t put in the logic to avoid issues with selecting the same bot (or empty players) because I wanted to keep the code simpler.

When the app starts it comes up in the game screen:


But I don’t see the error in logcat until I switch over to the player screen. There are no players shown, so I enter myself and my wife as players and click the start button:
image
When I click the start button, that’s when the exception is thrown.

Apologies for losing track of this thread, but on the plus side, I see what’s going on. It took me a bit to find, too, since it wasn’t clear right away.

Quick version:
You’re missing android:text="@={player.playerName}" on the @+id/edit_text_player_name <Edit Text> in player_list_item.xml.

Long version:
The database exception happens because the game sees you as trying to enter in the same player twice, meaning both players have the same ID. They both are seen as having the same ID because of how the app gets players in PennyDropDao:

// PennyDropDao.startGame(...)
val playerIds = players.map { player -> 
    getPlayer(player.playerName)?.playerId ?: insertPlayer(player)
}

We get players by name from the DB or create new versions. If two players have the same name (even if it’s a blank name), one is inserted into the DB and the other is retrieved right away, but they’re both the same.

In this issue’s case, the names were blank because nothing was in place to assign the name to the playerName field. That lives inside player_list_item.xml and the edit_text_player_name <EditText>.

If you don’t have a value set for android:text, a TextView will still show what you type in, but nothing happens with it. You need to set an expression there (with the = after the @) to link it back to the NewPlayer object:

android:text="@={player.playerName}"

While the error makes sense in the end, it’s almost misleading given where the issue starts out.

Not a problem on the delay in responding, I’ve had a few things going on here this summer that kept me from this as well.

You nailed the problem perfectly. After adding the missing attribute to the EditText element, the app is running without the exception.

Thank you so much!