Search code examples
javaandroidkotlinenumsandroid-room

Exception while computing database live data with Enum


I have added a data class and try to save it into Room. I went through stackoverflow and didn't find an answer.

So, the error is:

Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 2 path $
Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 2 path $

I am using Room 2.4.2 so enum is supposed to be supported.

The model I am using is :

@Entity(tableName = "userpreferencestable")
class UserPreferencesEntity (
    @PrimaryKey()
    var vin: String,
    @ColumnInfo(name = "control")
    var command: List<CommandTile?>
)

and CommandTile is defined as below:

data class CommandTile(
    @SerializedName("name")
    var name: DashboardTile.Name,
    @SerializedName("state")
    var state: DashboardTile.State
)

State and Name are enum and defined as below:

   enum class Name {
       NAME1,
       NAME2...
    }

    enum class State {
        TAT,
        TOT
    }

I have tried to add a DataConverter but it's not working.

 @TypeConverter
    fun fromName(name: Name): String {
        return name.name
    }

    @TypeConverter
    fun toName(name: String): Name {
        return Name.valueOf(name)
    }

    @TypeConverter
    fun fromState(state: State): String {
        return state.name
    }

    @TypeConverter
    fun toState(state: String):State {
        return State.valueOf(state)
    }


It still not working. I cannot figure out how to save the List of data class with enum.

Any idea ?


Solution

  • You issue is not the Enums, rather it is with the command List<CommandTile> (according to the disclosed code).

    TypeConverters are for converting from/to a column for the data to be stored/retrieved.

    As you have no @Entity annotation for the CommandTile class BUT instead have List<CommandTile?> as a column in the UserPrefrences class, which does have @Entity annotation, then Room will want to convert the List of CommandTiles to an acceptable type (in SQLite along with Room's restrictions this would have to be a type that resolves to one of TEXT (String), INTEGER (Int, Long ...), REAL (Double, Float ....) or BLOB (ByteArray).

    • types in parenthesise are Kotlin Types, they are examples and are not fully comprehensive.

    As an example, overcoming issue that you may encounter using List, consider the following:-

    A new class CommandTileList

    data class CommandTileList(
        val commandTileList: List<CommandTile>
    )
    
    • to avoid a column that is a List

    A modified UserPreferencesEntity class to use a CommandTileList rather than List<CommandTile>

    @Entity(tableName = "userpreferencestable")
    class UserPreferencesEntity (
        @PrimaryKey()
        var vin: String,
        @ColumnInfo(name = "control")
        var command: CommandTileList
    )
    

    the TypeConverters class, with appropriate TypeConverters

    class TypeConverters {
    
        @TypeConverter
        fun fromCommandTileToString(commandTileList: CommandTileList): String {
            return Gson().toJson(commandTileList)
        }
        @TypeConverter
        fun fromStringToCommandTile(string: String): CommandTileList {
            return Gson().fromJson(string,CommandTileList::class.java)
        }
    }
    

    A suitable @Dao annotated class AllDAO (for demo)

    @Dao
    interface AllDAO {
    
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(userPreferencesEntity: UserPreferencesEntity): Long
        @Query("SELECT * FROM userpreferencestable")
        fun getAllUserPreferences(): List<UserPreferencesEntity>
    }
    

    A suitable @Database annotated class TheDatabase (for demo) noting the TypeConverters class being defined with full scope via the @TypeConverters annotation (not the plural rather than singular form)

    @TypeConverters(value = [TypeConverters::class])
    @Database(entities = [UserPreferencesEntity::class], version = 1, exportSchema = false)
    abstract class TheDatabase: RoomDatabase() {
        abstract fun getAllDAO(): AllDAO
    
        companion object {
            var instance: TheDatabase? = null
            fun getInstance(context: Context): TheDatabase {
                if (instance == null) {
                    instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
                        .allowMainThreadQueries()
                        .build()
                }
                return instance as TheDatabase
            }
        }
    }
    
    • .allowMainThreadQueries for convenience/brevity

    Finally putting it all into action witin an Activty MainActivity

    class MainActivity : AppCompatActivity() {
    
        lateinit var db: TheDatabase
        lateinit var dao: AllDAO
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            val c1 = CommandTile(DashboardTile.Name.NAME1,DashboardTile.State.TAT)
            val c2 = CommandTile(DashboardTile.Name.NAME2,DashboardTile.State.TOT)
    
            db = TheDatabase.getInstance(this)
            dao = db.getAllDAO()
            dao.insert(userPreferencesEntity = UserPreferencesEntity("VIN1", CommandTileList(listOf(
            c1,c2))))
            for(u in dao.getAllUserPreferences()) {
                Log.d("DBINFO","VIV = ${u.vin} CommandTiles in Command = ${u.command.commandTileList.size} They Are:-")
                for (ct in u.command.commandTileList) {
                    Log.d("DBINFO","\tName = ${ct.name} State = ${ct.state}")
                }
            }
        }
    }
    

    Result

    The log includes:-

    D/DBINFO: VIV = VIN1 CommandTiles in Command = 2 They Are:-
    D/DBINFO:   Name = NAME1 State = TAT
    D/DBINFO:   Name = NAME2 State = TOT
    

    The Database, via App Inspection :-

    enter image description here

    As you can see the list of 2 CommandTiles (a CommandTileList) has been converted to a String (SQLite type TEXT) and stored and subsequently retrieved.

    Note from a database perspective this is not ideal it

    • limits/complicates the usefulness of the stored data.

      • For example (simple) if you wanted to select all States that start with A then SELECT * FROM userpreferencestable WHERE command LIKE 'A%' would find all columns you would have to use some like SELECT * FROM userpreferencestable WHERE 'state":A%'.
    • introduces bloat with all that extra data and thus introduces inefficiencies.

    • breaks normalisation as the same values are stored multiple times

    The database way would be to have a table based upon the CommandTile incorporating the suitable relationship between CommandTiles and the UserPreferences.

    • Then there would be no need for the TypeConverter.