Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android Room type convert multiple enum types

I am writing a type converter for my Room database. I have a couple custom enum classes and I want to convert all of them to its ordinals when stored in the database. So, instead of writing the following for every single class, is there any way to simplify it (such as pass in a generic enum type)?

class Converter {

    @TypeConverter
    fun toOrdinal(type: TypeA): Int = type.ordinal

    @TypeConverter
    fun toTypeA(ordinal: Int): TypeA = TypeA.values().first { it.ordinal == ordinal }

    @TypeConverter
    fun toOrdinal(type: TypeB): Int = type.ordinal

    @TypeConverter
    fun toTypeB(ordinal: Int): TypeB = TypeB.values().first { it.ordinal == ordinal }

    ...
}
like image 850
Jack Guo Avatar asked Jun 29 '18 13:06

Jack Guo


2 Answers

As discussed here, Room can't handle generic converters at the moment. I think the best you can do is create extensions that make writing these enum converters quicker:

@Suppress("NOTHING_TO_INLINE")
private inline fun <T : Enum<T>> T.toInt(): Int = this.ordinal

private inline fun <reified T : Enum<T>> Int.toEnum(): T = enumValues<T>()[this]

This would simplify each pair of converters to this code:

@TypeConverter fun myEnumToTnt(value: MyEnum) = value.toInt()
@TypeConverter fun intToMyEnum(value: Int) = value.toEnum<MyEnum>()

Or if you might be storing null values:

@TypeConverter fun myEnumToTnt(value: MyEnum?) = value?.toInt()
@TypeConverter fun intToMyEnum(value: Int?) = value?.toEnum<MyEnum>()
like image 157
zsmb13 Avatar answered Nov 10 '22 07:11

zsmb13


You can use a composition interface to achieve this as you cannot write a single converter class for multiple object types. It is kind of hacky but might just work:

interface BaseType {
    val arg0: String

    fun asString() : String? {
        return when(this) {
            is TypeA -> "${TypeA::class.simpleName}$separatorParam$arg0"
            is TypeB -> "${TypeB::class.simpleName}$separatorParam$arg0"
            else -> null
        }
    }

    companion object {
        const val separatorParam = "::"
    }
}

enum class TypeA (override val arg0: String) : BaseType {
    A_ONE("argument 1"),
    A_TWO("argument 2");

    companion object {
        fun getValueTypes(arg0: String) : TypeA? = values().firstOrNull { it.arg0 == arg0 }
    }
}

enum class TypeB (override val arg0: String) : BaseType {
    A_ONE("argument 1"),
    A_TWO("argument 2");

    companion object {
        fun getValueTypes(arg0: String) : TypeB? = values().firstOrNull { it.arg0 == arg0 }
    }
}

class Converter {
    @TypeConverter
    fun fromBaseType(type: BaseType) : String? = type.asString()

    @TypeConverter
    fun toBaseType(param: String?) : BaseType? = param?.asBaseType()

    private fun String.asBaseType() : BaseType? {
        val stringArray = this.split(BaseType.separatorParam)
        val className : String? = stringArray[0]
        return when(className) {
            TypeA::class.simpleName -> TypeA.getValueTypes(stringArray[1])
            TypeB::class.simpleName -> TypeB.getValueTypes(stringArray[1])
            else -> null
        }
    }
}

Then you need a function in your data class to provide you the actual TypeA or TypeB

data class MyDbModel(val baseType: BaseType) {
    inline fun <reified T: BaseType> getTypeAs() : T? = 
            when(baseType) {
                is TypeA -> TypeA.getValueTypes(baseType.arg0) as? T
                is TypeB -> TypeB.getValueTypes(baseType.arg0) as? T
                else -> null
            }
}

fun foo() {
    val model = MyDbModel(TypeA.A_ONE)
    val type = model.getTypeAs<TypeA>()
}

The disadvantage of this is that it works only for unique arg0 within the specific enum, for that you could use ordinal or you could use generated ID like R.id.a_one as the first parameter and then the second parameter could be your string.

like image 1
HawkPriest Avatar answered Nov 10 '22 06:11

HawkPriest