but I couldn't do it right " /> but I couldn't do it right " /> but I couldn't do it right "/>

How to do converter ArrayList<Bitmap> for room in kotlin?

49 Views Asked by At

Hi I need to store array of Bitmaps in Room database like

@ColumnInfo(name = "imageList")
    var imageList: ArrayList<Bitmap>

but I couldn't do it right way because of my converter I think.it only puts first element in bitmap list. How can I solve that?

class BitmapListConverter {
    @TypeConverter
    fun toBitmapList(bitmap: Bitmap): ArrayList<Bitmap> {
        return arrayListOf(bitmap)
    }

    @TypeConverter
    fun fromBitmapList(array:ArrayList<Bitmap>): Bitmap {
        val bmp = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) 
        return if (array.isEmpty()) bmp else array.first() //this causing issue
    }
}
1

There are 1 best solutions below

0
MikeT On

With SQLite and therefore Room a column can contain just 1 item of data. A Type converter must convert from whatever to a single unit of data that can be an integer type value (INTEGER type in SQLite), a decimal type value (REAL type), a null (useless unless indicating no data), a stream of characters (TEXT type) or a stream of bytes (BLOB).

Null, INTEGER and REAL types are probably of little use for storing a list of something.

You are expecting many bitmaps to be saved as a single BITMAP but are only getting the first. If you could append the bitmaps into many bitmaps then that would work. Perhaps some bitmap handling libraries/utilities could accomplish this. However, you would then also need to dismantle/split the single bitmap into many. This would probably be quite some task as it is not very likely that a bitmap can hold the additional information needed to dismantle the many bitmaps.

What I would suggest is to have a table (an @Entity annotated class) in which a single bitmap is stored per row. A bitmap can then be a child to the parent table and you would have a POJO class that would have the parent (@Entity annotated class) field annotated with @Embedded and the ArrayList field annotated with @Relation.

A decision would have to be made as to the type of relationship. It could be a 1 parent with many children, in which case the child @Entity (the bitmap) could have a field to uniquely identify the parent of the child.

  • this would appear to suffice but has the disadvantage that if the same bitmap is used by many parents that the bitmap would have to stored multiple times (basically duplicating the stored bitmap)

The alternative could be to have a many-many relationship where a single bitmap could have many parents and many parents could be related to many children (bitmaps). Such a relationship is accomplished by having a third table that has two core columns/fields; one for the unique identifier of the parent; the other for the unique identifier of the bitmap. The two columns combined would form the primary key (noting that the primaryKey parameter of the @Entity annotation has to be used to specify this composite primary key).

For this many-many relationship @Relation annotation in the POJO differ in that it would include the associateBy parameter to specify the Junction (the name of columns in the third table that uniquely identify the parent and the child).


Demo


The following demonstrates both a 1-many and a many-many method.

  • It should be noted that the bitmap handling has been simplified, but that a type converters were required. They too are very simple but importantly just a single bitmap is stored.

  • The complication is that both methods have been included.

The Code

First the Parent that itself DOES NOT include the troublesome List (ArrayList):-

@Entity
data class Parent(
    @PrimaryKey
    var parentId: Long?=null,
    /* Other columns */
    var name: String
)

Next the Entity for storing a single bitmap per row for the the 1-Many version. i.e. it's parent is stored and thus a bitmap can only have the 1 parent:-

@Entity
data class BM_1_to_many(
    @PrimaryKey
    var bm_1_to_manyId: Long?=null,
    var parentIdMap: Long,
    var bitmap: Bitmap
)

To combine the Parent with the child bitmaps the POJO :-

data class ParentWithListOfBM_1_to_many(
    @Embedded
    var parent: Parent,
    @Relation(
        entity = BM_1_to_many::class /* optional in this case as implied via ListOfBM_1_to_many field */,
        parentColumn = "parentId",
        entityColumn = "parentIdMap"
    )
    var ListOfBM_1_to_many: List<BM_1_to_many>
)

*For the Many-Many version

The entity for storing single bitmaps:-

@Entity
data class BM_many_to_many(
    @PrimaryKey
    var bm_many_to_manyId: Long?=null,
    var bitmap: Bitmap
)
  • i.e. no need to store the parentid

The third (associative/mapping and other names .... table):-

@Entity( primaryKeys = ["parentIdMap","bm_many_to_manyIdMap"])
data class BM_many_many_associative_table(
    var parentIdMap: Long,
    @ColumnInfo(index = true)
    var bm_many_to_manyIdMap: Long
)
  • i.e. the 2 columns, the composite primary key and not mentioned the 2nd column is indexed (Room will warn if not).

To combine the Parent with the child bitmaps the POJO with the extended associateBy parameter:-

data class ParentWithListOfBM_many_to_many(
    @Embedded
    var parent: Parent,
    @Relation(
        entity = BM_many_to_many::class,
        parentColumn = "parentId",
        entityColumn = "bm_many_to_manyId",
        associateBy = Junction(
            BM_many_many_associative_table::class,
            parentColumn = "parentIdMap",
            entityColumn = "bm_many_to_manyIdMap"
        )
    )
    var ListOfBM_many_to_many: List<BM_many_to_many>
)

The Type Converter class with the from and to converters (very simple just to demonstrate):-

class TC {
    @TypeConverter
    fun convertBitMapToByteArray(bitmap: Bitmap) :ByteArray = ByteArray(100)
    @TypeConverter
    fun convertByteArrayToBitmap(byteArray: ByteArray): Bitmap = Bitmap.createBitmap(100,100,Bitmap.Config.ARGB_8888)
}

The @Dao interface that allows data to be inserted and extracted (as parent with the list of bitmaps for each version/method):-

@Dao
interface TheDAOs{
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(parent: Parent): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(bm1ToMany: BM_1_to_many): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(bmManyToMany: BM_many_to_many): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(bmManyManyAssociativeTable: BM_many_many_associative_table): Long
    @Transaction
    @Query("SELECT * FROM parent")
    fun getAllParentsWithTheirBM1To1List(): List<ParentWithListOfBM_1_to_many>
    @Transaction
    @Query("SELECT * FROM parent")
    fun getAllParentsWithThierBMManyToManyList(): List<ParentWithListOfBM_many_to_many>
}

A simple @Database annotated abstract class. Noting that for brevity the main thread has been opened up for use:-

@TypeConverters(TC::class)
@Database(entities = [Parent::class,BM_1_to_many::class,BM_many_to_many::class,BM_many_many_associative_table::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
    abstract fun getTheDAOs(): TheDAOs
    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
        }
    }
}
  • Note the @TypeConverts *PLURAL annotation to provide the fullest scope of the Type Converters.

Finally some activity code to actually demonstrate inserting and extracting data replicating the parent/child for each method/version:-

class MainActivity : AppCompatActivity() {
    lateinit var db: TheDatabase
    lateinit var dao: TheDAOs
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val TAG = "DBINFO"
        db = TheDatabase.getInstance(this)
        dao = db.getTheDAOs()
        val bm001 = Bitmap.createBitmap(100,100,Bitmap.Config.ARGB_8888)
        val p1id = dao.insert(Parent(name = "P1"))
        val p2id = dao.insert(Parent(name = "P2"))
        val p3id = dao.insert(Parent(name = "P3"))

        dao.insert(BM_1_to_many(parentIdMap = p1id, bitmap = bm001))
        dao.insert(BM_1_to_many(parentIdMap = p1id, bitmap = bm001))
        dao.insert(BM_1_to_many(parentIdMap = p1id, bitmap = bm001))
        dao.insert(BM_1_to_many(parentIdMap = p2id, bitmap = bm001))
        dao.insert(BM_1_to_many(parentIdMap = p2id, bitmap = bm001))
        dao.insert(BM_1_to_many(parentIdMap = p3id, bitmap = bm001))

        val bmmtmid01 = dao.insert(BM_many_to_many(bitmap = bm001))
        val bmmtmid02 = dao.insert(BM_many_to_many(bitmap = bm001))
        val bmmtmid03 = dao.insert(BM_many_to_many(bitmap = bm001))

        dao.insert(BM_many_many_associative_table(p1id,bmmtmid01))
        dao.insert(BM_many_many_associative_table(p1id,bmmtmid02))
        dao.insert(BM_many_many_associative_table(p1id,bmmtmid03))
        dao.insert(BM_many_many_associative_table(p2id,bmmtmid01))
        dao.insert(BM_many_many_associative_table(p2id,bmmtmid02))
        dao.insert(BM_many_many_associative_table(p3id,bmmtmid03))

        for (pwbm121 in dao.getAllParentsWithTheirBM1To1List()) {
            val sb = StringBuilder()
            for (bm in pwbm121.ListOfBM_1_to_many) {
                sb.append("\n\tBM's ID is ${bm.bm_1_to_manyId} Parent ID is ${bm.parentIdMap} BM is ${bm.bitmap}")
            }
            Log.d(TAG,"Parent ID is ${pwbm121.parent.parentId} NAME is ${pwbm121.parent.name}. It has ${pwbm121.ListOfBM_1_to_many.size} bitmaps. They are:-${sb}")
        }

        for (pwbmM2M in dao.getAllParentsWithThierBMManyToManyList()) {
            val sb = StringBuilder()
            for (bm in pwbmM2M.ListOfBM_many_to_many) {
                sb.append("\n\tBM's ID is ${bm.bm_many_to_manyId} (Parent ID not in the BMM2M) BM is ${bm.bitmap} ")
            }
            Log.d(TAG,"Parent ID is ${pwbmM2M.parent.parentId} NAME is ${pwbmM2M.parent.name}. It has ${pwbmM2M.ListOfBM_many_to_many.size} bitmaps. They are:-${sb}")
        }
    }
}

Results

The log includes:-

For the 1-many method:-

2023-12-03 21:38:30.886 D/DBINFO: Parent ID is 1 NAME is P1. It has 3 bitmaps. They are:-
        BM's ID is 1 Parent ID is 1 BM is android.graphics.Bitmap@3574073
        BM's ID is 2 Parent ID is 1 BM is android.graphics.Bitmap@dbbab30
        BM's ID is 3 Parent ID is 1 BM is android.graphics.Bitmap@e30b3a9
2023-12-03 21:38:30.886 D/DBINFO: Parent ID is 2 NAME is P2. It has 2 bitmaps. They are:-
        BM's ID is 4 Parent ID is 2 BM is android.graphics.Bitmap@f302d2e
        BM's ID is 5 Parent ID is 2 BM is android.graphics.Bitmap@4db70cf
2023-12-03 21:38:30.886 D/DBINFO: Parent ID is 3 NAME is P3. It has 1 bitmaps. They are:-
        BM's ID is 6 Parent ID is 3 BM is android.graphics.Bitmap@ce84a5c

For the many-many method:-

2023-12-03 21:38:30.888 D/DBINFO: Parent ID is 1 NAME is P1. It has 3 bitmaps. They are:-
        BM's ID is 1 (Parent ID not in the BMM2M) BM is android.graphics.Bitmap@4b7e165 
        BM's ID is 2 (Parent ID not in the BMM2M) BM is android.graphics.Bitmap@215ee3a 
        BM's ID is 3 (Parent ID not in the BMM2M) BM is android.graphics.Bitmap@cc84aeb 
2023-12-03 21:38:30.889 D/DBINFO: Parent ID is 2 NAME is P2. It has 2 bitmaps. They are:-
        BM's ID is 1 (Parent ID not in the BMM2M) BM is android.graphics.Bitmap@4ddb048 
        BM's ID is 2 (Parent ID not in the BMM2M) BM is android.graphics.Bitmap@dca2ee1 
2023-12-03 21:38:30.889 D/DBINFO: Parent ID is 3 NAME is P3. It has 1 bitmaps. They are:-
        BM's ID is 3 (Parent ID not in the BMM2M) BM is android.graphics.Bitmap@c64b406 
  • As can be seen P1 has 3 bitmaps, P2 2 and P3 1 for both.
  • However for the 1-many there are 6 bitmap rows, whilst only 3 for the many-many (assuming duplicates)
    • note the ....Bitmap@??????? is different as it's the objects memory location NOT THE ACTUAL BITMAP DATA (i.e. part of the simplicity of the demo).

Using App Inspection:-

enter image description here

  • both methods use the same Parent table

enter image description here

  • the 1-many version with the id of the parent included

enter image description here

  • the many-many with just the 3 rows and no parent stored

enter image description here

  • The associative table with the 6 relationships between parent and children

Doing it as per the Question

Instead of storing the bitmaps directly as bitmaps they could be stored as a JSON representation of the ArrayList. This would require utilising a Gson library for the type conversion. The JSON represents the objects as a string and thus adds bloat.

Again a simple demo.

First a class for the BitMap arrayList (just a POJO that is included in the Entity):-

data class BitMapList(
    var bitMapList: ArrayList<Bitmap>
)

And now the Entity;-

@Entity
data class ParentWithBitmapArrayList(
    @PrimaryKey
    var parentId: Long?=null,
    var name: String,
    var bitmapList: BitMapList
)

2 Type Converter functions:-

@TypeConverter
fun convertToJSONString(bitmapList: BitMapList): String = Gson().toJson(bitmapList)
@TypeConverter
fun convertFromJSONString(jsonString: String): BitMapList = Gson().fromJson(jsonString, BitMapList::class.java)

Some additional @Dao functions:-

@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(parentWithBitmapArrayList: ParentWithBitmapArrayList): Long
@Query("SELECT * FROM parentwithbitmaparraylist")
fun getAllPWBML(): List<ParentWithBitmapArrayList>

The amended @Database annotation to include the additional table/entity:-

@Database(
    entities = [
        Parent::class,BM_1_to_many::class,
        BM_many_to_many::class,
        BM_many_many_associative_table::class,
        ParentWithBitmapArrayList::class
               ],
    version = 1,
    exportSchema = false
)

Finally some extra activity code:-

    dao.insert(ParentWithBitmapArrayList(null, name= "P1", BitMapList(arrayListOf(bm001,bm001,bm001))))
    dao.insert(ParentWithBitmapArrayList(name = "P2",bitmapList = BitMapList(arrayListOf(bm001,bm001))))
    dao.insert(ParentWithBitmapArrayList(parentId = null, name="P3", bitmapList =  BitMapList(arrayListOf(bm001))))
    dao.insert(ParentWithBitmapArrayList(name="P4",parentId=null, bitmapList =  BitMapList(arrayListOf())))

    for (pwbal in dao.getAllPWBML()) {
        val sb = StringBuilder()
        for (bm in pwbal.bitmapList.bitMapList) {
            sb.append("\n\t${bm}")
        }
        Log.d(TAG,"Parent ID is ${pwbal.parentId}. It has ${pwbal.bitmapList.bitMapList.size} bitmaps. They are:-${sb}")
    }
  • i.e. add 4 rows (the last having 0 bitmaps)

The result being:-

2023-12-04 09:53:18.839 D/DBINFO: Parent ID is 1. It has 3 bitmaps. They are:-
        android.graphics.Bitmap@2183dde
        android.graphics.Bitmap@ee02bbf
        android.graphics.Bitmap@781028c
2023-12-04 09:53:18.839 D/DBINFO: Parent ID is 2. It has 2 bitmaps. They are:-
        android.graphics.Bitmap@4fcedd5
        android.graphics.Bitmap@7e659ea
2023-12-04 09:53:18.840 D/DBINFO: Parent ID is 3. It has 1 bitmaps. They are:-
        android.graphics.Bitmap@175c4db
2023-12-04 09:53:18.840 D/DBINFO: Parent ID is 4. It has 0 bitmaps. They are:-

The table, via App Inspection:-

enter image description here

  • Note at a guess "mNativePtr":3865181248 is a compressed form of the 100 * 100 bitmap.

Note

Although SQLite can be quite effective and efficient at storing large amounts of contiguous data. Storing such data, especially on Android devices can be very inefficient due to the underlying API and available resources. Storing images is typically frowned upon rather it is frequently advised that the images be stored as files and that the path or part thereof is stored in the database.