My data on firebase uses many fields which have string type, but really are enum values (which I check in my validation rules). To download the data into my Android app, following the guide, the field must be a basic String
. I know I can work around this with a second (excluded) field which is an enum, and set this basing on the string value. A short example:
class UserData : BaseModel() {
val email: String? = null
val id: String = ""
val created: Long = 0
// ... more fields omitted for clarity
@Exclude
var weightUnitEnum: WeightUnit = WeightUnit.KG
var weightUnit: String
get() = weightUnitEnum.toString()
set(value) { weightUnitEnum = WeightUnit.fromString(value) }
}
enum class WeightUnit(val str: String) {
KG("kg"), LB("lb");
override fun toString(): String = str
companion object {
@JvmStatic
fun fromString(s: String): WeightUnit = WeightUnit.valueOf(s.toUpperCase())
}
}
Now, while this works, it's not really clean:
- The
enum class
itself is (1) kinda long for an
enum, (2) the insides are repeated for every enum. And I have more of them.
- It's not only enums, the
created
field above is really a timestamp,
not a Long
.
- Each model uses these enum fields a lot of times, which bloats the model classes with repeatable code...
- The helper field/functions are getting much worse/longer for fields with types such as
Map<SomeEnum, Timestamp>
...
So, is there any way to do this properly? Some library maybe? Or some way to write a magic "field wrapper" that would automatically convert strings to enums, or numbers to timestamps, and so on, but is still compatible with Firebase library for getting/setting data?
(Java solutions are welcome too :) )
If the conversion between a property with your enum
value and another property of String
type is enough, this can be easily done in a flexible way using Kotlin delegated properties.
To say it short, you can implement a delegate for String
properties which performs the conversion and actually gets/sets the value of another property storing the enum
values, and then delegate the String
property to it.
One possible implementation would look like this:
class EnumStringDelegate<T : Enum<T>>(
private val enumClass: Class<T>,
private val otherProperty: KMutableProperty<T>,
private val enumNameToString: (String) -> String,
private val stringToEnumName: (String) -> String) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return enumNameToString(otherProperty.call(thisRef).toString())
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
val enumValue = java.lang.Enum.valueOf(enumClass, stringToEnumName(value))
otherProperty.setter.call(thisRef, enumValue)
}
}
Note: This code requires you to add the Kotlin reflection API, kotlin-reflect
, as a dependency to your project. With Gradle, use compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
.
This will be explained below, but first let me add a convenience method to avoid creating the instances directly:
inline fun <reified T : Enum<T>> enumStringLowerCase(
property: KMutableProperty<T>) = EnumStringDelegate(
T::class.java,
property,
String::toLowerCase,
String::toUpperCase)
And a usage example for your class:
// if you don't need the `str` anywhere else, the enum class can be shortened to this:
enum class WeightUnit { KG, LB }
class UserData : BaseModel() {
// ... more fields omitted for clarity
@Exclude
var weightUnitEnum: WeightUnit = WeightUnit.KG
var weightUnit: String by enumStringLowerCase(UserData::weightUnitEnum)
}
Now, the explanation:
When you write var weightUnit: String by enumStringLowerCase(UserData::weightUnitEnum)
, you delegate the String
property to the constructed delegate object. This means that when the property is accessed, the delegate methods are called instead. And the delegate object, in turn, works with the weightUnitEnum
property under the hood.
The convenience function I added saves you from the necessity of writing UserData::class.java
at the property declaration site (using a reified type parameter) and provides the conversion functions to EnumStringDelegate
(you can create other functions with different conversions at any time, or even make a function that receives the conversion functions as lambdas).
Basically, this solution saves you from the boilerplate code that represents a property of enum
type as a String
property, given the conversion logic, and also allows you to get rid of the redundant code in your enum
, if you don't use it anywhere else.
Using this technique, you can implement any other conversion between properties, like the number to timestamp you mentioned.
I am in similar situation & thus found your question, plus whole lot of other similar questions/answers.
Cant answer your question directly but this is what I ended up doing: I decided to change my app & not use enum data types at all - mainly because of the advice from Google dev portal which shows how bad the enum's are on app's performance. See the video below https://www.youtube.com/watch?v=Hzs6OBcvNQE