Home > Software design >  Why are my TypeConverters not recognized/working? Kotlin/ Android
Why are my TypeConverters not recognized/working? Kotlin/ Android

Time:06-18

I am implementing TypeConverters for date? This seems wrong as java.util.date is deprecated. Kotlinx.datetime.LocalDate is supposed to be serializable but is also not working.

Task: add expiration and start dates to an Entity in Room that will come from the date picker so I imagine will return a Calendar which I also tried to no avail.

I am getting this error

Bottle.java:23: error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.
    private final java.util.Date expirationDate = null;

On this generated portion


   21  @org.jetbrains.annotations.Nullable
   22  @androidx.room.ColumnInfo
   23  private final java.util.Date expirationDate = null;
   24  @org.jetbrains.annotations.Nullable
   25  @androidx.room.ColumnInfo
   26  private java.util.Date startDate;

Bottle class:

@Entity(tableName = "bottles")
@Serializable

data class Bottle(
    @PrimaryKey (autoGenerate = true)
    @ColumnInfo val bottleID : Int?,
    @ColumnInfo val consumableID: String,
    @ColumnInfo  @Contextual val expirationDate : Date?,
    @ColumnInfo @Contextual var startDate : Date?,
    @ColumnInfo val cabinetID: String
)

DataBase

@Database(entities = [
    Cabinet::class,
    Bottle::class,
    Consumable::class,
], version = 1)

@TypeConverters(Converters::class)

abstract class PillMinderDatabase : RoomDatabase() {
    abstract val cabinetDAO: CabinetDAO
    abstract val bottleDAO: BottleDAO
    abstract val consumableDAO: ConsumableDAO

}

Converters

class Converters {
    companion object {

//**************************** CALENDAR CONVERTERS ****************************//
 
        @TypeConverter
        @JvmStatic
        fun dateToLong(date: Date?): Long? = date?.time

        @TypeConverter
        @JvmStatic
        fun longToDate(dateInMillis: Long?): Date? = dateInMillis?.let { Date(dateInMillis)}

   }
}


  [1]: https://i.stack.imgur.com/PsO5F.png

CodePudding user response:

Your issue is that you are using a companion object and Room doesn't expect to utilise static methods when it builds the underlying code.

To resolve this issue, use :-

class Converters {

        @TypeConverter
        fun dateToLong(date: Date?): Long? = date?.time
        @TypeConverter
        fun longToDate(dateInMillis: Long?): Date? = dateInMillis?.let { Date(dateInMillis)}
}

However, if saving the date as a Long, then there is no need for TypeConverters you just pass and extract as a Long.

add expiration and start dates to an Entity in Room that will come from the date picker so I imagine will return a Calendar which I also tried to no avail.

You can use the DatePicker's year, month and dayOfMonth to get the values.

Working Example

Here's a working example that has your Bottle class and also an AltBottle class (with just Long's, so no TypeConverters) that has a DatePicker and a Button to insert both a Bottle and an AltBottle. It also includes a ListView that displays the startDate and the expirationDate.

The following are the components that the example is comprised of:-

The Bottle and AltBottle classes:-

@Entity(tableName = "bottles")
@Serializable

data class Bottle(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo val bottleID : Int?,
    @ColumnInfo val consumableID: String,
    @ColumnInfo  @Contextual val expirationDate : Date?,
    @ColumnInfo @Contextual var startDate : Date?,
    @ColumnInfo val cabinetID: String
)


@Entity
data class AltBottle(
    /* No need for @Serializable or for @ColumnInfo's without any parameters */
    @PrimaryKey
    var altBottleId: Long? = null, /* it is more correct to use a Long rather than Int for an id, it is 64bit signed and can outgrow an Int */
    var altComsunableId: String,
    var altExpirationDate: Long?,
    var altStartDate: Long?,
    var cabinetId: String
)

The Converters Class (not needed for the AltBottle):-

class Converters {

        @TypeConverter
        fun dateToLong(date: Date?): Long? = date?.time
        @TypeConverter
        fun longToDate(dateInMillis: Long?): Date? = dateInMillis?.let { Date(dateInMillis)}
}

An @Dao annotated interface AllDao :-

@Dao
interface AllDao {

    @Insert
    fun insert(bottle: Bottle): Long
    @Insert
    fun insert(altBottle: AltBottle): Long

    @Query("SELECT * FROM bottles")
    fun getAllBottles(): List<Bottle>
    @Query("SELECT * FROM altBottle")
    fun getAllAltBottles(): List<AltBottle>
}
  • Note that the 2 query functions aren't actually used as for simplicity the data is extracted as a Cursor for driving the ListView (see MainActivity)

An @Database annotated class TheDatabase (note .allowMainThreadQueries used for convenience and brevity):-

@Database(entities = [Bottle::class,AltBottle::class], version = 1, exportSchema = false)
@TypeConverters(value = [Converters::class])
abstract class TheDatabase: RoomDatabase() {
    abstract fun getAllDao(): AllDao

    companion object {
        private 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
        }
    }
}

A Layout for MainActivity that includes a DatePicker, a Button and a ListView for the results :-

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!">
    </TextView>

    <DatePicker
        android:id="@ id/datepicker"
        android:datePickerMode="calendar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    </DatePicker>
    <Button
        android:id="@ id/insert"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="INSERT"
        >
    </Button>
    <ListView
        android:id="@ id/listview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    </ListView>

</LinearLayout>

Finally MainActivity :-

class MainActivity : AppCompatActivity() {
    lateinit var db: TheDatabase
    lateinit var dao: AllDao
    lateinit var lv: ListView
    lateinit var btn: Button
    var sca: SimpleCursorAdapter? = null
    var csr: Cursor? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val dp = this.findViewById<DatePicker>(R.id.datepicker)
        lv = this.findViewById(R.id.listview)
        btn = this.findViewById(R.id.insert)

        /* Prepare DB access */
        db = TheDatabase.getInstance(this)
        dao = db.getAllDao()

        /* Prepare the Button for inserting rows */
        val button = this.findViewById<Button>(R.id.insert)
        button.setOnClickListener { insertData(dp) }
        /* setup the ListView */
        setOrRefreshListView()

    }

    /* Insert the data (1 row for bottles table and an identical row the the altBottle table)
    *     Note that this also refreshes the ListView
    * */
    fun insertData(datePicker: DatePicker) {
        var cal: Calendar = Calendar.getInstance()
        cal.set(datePicker.year,datePicker.month,datePicker.dayOfMonth)

        dao.insert(Bottle(null,"Blah",cal.time,cal.time,"blah"))
        dao.insert(AltBottle(null,"AltBlah",cal.timeInMillis,cal.timeInMillis,"AltBlah"))
        setOrRefreshListView()
    }

    /* Setup or Refresh the ListView */
    fun setOrRefreshListView() {
        csr = db.getOpenHelper().writableDatabase.query("SELECT *,bottleId AS ${BaseColumns._ID} FROM bottles UNION All SELECT *,altBottleId AS ${BaseColumns._ID} FROM altbottle")
        if (sca== null) {
            sca = SimpleCursorAdapter(this,android.R.layout.simple_list_item_2, csr,arrayOf("expirationDate","startDate"), intArrayOf(android.R.id.text1,android.R.id.text2),0)
            lv.adapter = sca
        } else {
            sca!!.swapCursor(csr)
        }
    }
}
  • See the insertData function for how the data is retrieved from the DatePicker

Results

When installed/started for the first time then the App displays:-

enter image description here

If 1st June is selected and then the INSERT button is clicked :-

enter image description here

As can be see two rows have been added.

Select 21 June and click on INSERT and another 2 rows are added :-

enter image description here

  • Note that the rows from the bottles table appear together and are then followed by the rows from the altBottle table.

and so on ....

Using App Inspection then the bottles table is :-

enter image description here

and the altBottle table is :-

enter image description here

CodePudding user response:

import java.util.Date

class Converters {
    @TypeConverter
    fun dateToLong(date: Date?): Long? = date?.time

    @TypeConverter
    fun longToDate(dateInMillis: Long?): Date? = dateInMillis?.let { Date(dateInMillis) }
}
  • Related