Firebase: clean way for using enum fields in Kotli

2019-06-16 19:34发布

问题:

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 :) )

回答1:

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.



回答2:

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