https://kotlinlang.org logo
Title
d

Daniele Segato

09/11/2018, 7:13 PM
I've several enum that I use to map with JSON values from GSon and I've this piece of code that I've already copied 4 times at least and slightly edited... Is there a way to extract the part of the
companion object
into some kind of reusable code? The main problem is I need the
Enum.values()
statically and the class name for strings. I also need to access the
jsonValue
in the enum, but I can put it in an interface of the enum I guess, so that's not a big deal... Any suggestion? See the code below (please answer in thread)
(still need to migrate to Moshi, but I doub't it would change anything)
k

karelpeeters

09/11/2018, 7:16 PM
I believe companion objects can extend classes, so maybe you can do something with that?
a

agrosner

09/11/2018, 7:17 PM
just make a generic abstract class with
T: Enum
or something, then move those methods into said class and have the companion implement it.
d

Daniele Segato

09/11/2018, 7:17 PM
I would find it acceptable even to do something like:
companion object {
  val myConverter = Converter(SomeType::class)
}
and use the converter for function and type adapter... extending on the companion object would just make it cleaner. even if I try to write a plain class that does that I've issues.... The problem is with the static usage of Enum
a

Andreas Sinz

09/11/2018, 7:20 PM
can you post the code of another part?
d

Daniele Segato

09/11/2018, 7:21 PM
what other part?
a

Andreas Sinz

09/11/2018, 7:22 PM
you said you have this code duplicated in at least 4 places, how does those look like?
d

Daniele Segato

09/11/2018, 7:23 PM
instead of
SomeType
there is
SomeOtherType
Take this: what should I put instead of the
???
class MyConverter<E: Enum<E>>(val clazz: KClass<E>) {
    init {
        require(clazz.isInstance(JsonValueEnum::class)) { "Not a JsonValueEnum" }
    }
    fun convertFromJsonValue(jsonValue: String): E {
        return try {
            // what should I put here?
            ???.values().first { (it as JsonValueEnum).jsonValue == jsonValue }
        } catch (e: NoSuchElementException) {
            throw NoSuchElementException("Cannot find a ${clazz.simpleName} for jsonValue $jsonValue")
        }
    }
    inner class MyAdapter : TypeAdapter<E?>() {
        override fun write(output: JsonWriter?, value: E?) {
            if (value != null) output?.jsonValue((value as JsonValueEnum).jsonValue) else output?.nullValue()
        }

        override fun read(input: JsonReader?): E? {
            val strValue = input?.nextString()
            return if (strValue != null) convertFromJsonValue(strValue) else null
        }
    }

    val GSON_TYPE_ADAPTER by lazy { MyAdapter() }
}
a

Andreas Sinz

09/11/2018, 7:35 PM
interface JsonValue {
    val jsonValue: String
}

interface JsonConverter<T: JsonValue> {    
    fun getValues(): Array<T>
    
    fun convertFromJsonType(jsonValue: String): T {
        return try {
            getValues().first { it.jsonValue == jsonValue }
        } catch (e: NoSuchElementException) {
            throw NoSuchElementException("Cannot find a SomeType for jsonValue $jsonValue")
        }
    }
}

enum class SomeType(override val jsonValue: String): JsonValue {
   // X = enum name, a = value in JSON
   X("a"),
   Y("b"),
   ;
    
   // now this is the annoying part...
   companion object : JsonConverter<SomeType>  {
       override fun getValues() = SomeType.values()
   }
}
d

Daniele Segato

09/11/2018, 7:38 PM
oh well... yeah that kind of work around the issue but works
a

Andreas Sinz

09/11/2018, 7:54 PM
or you could just use
enumValues<T>
with a Function Type, e.g.
class MyConverter<E: Enum<E>>(val clazz: KClass<E>, val values: () -> Array<E>) where E: JsonValue
and a factory method
inline fun createConverter<reified E: Enum<E>>() where E: JsonValue = MyConverter { enumValues<T>() }
then you can get the values inside the Converter with
values()
and the compiler knows that every value has a
val jsonValue: String
d

Daniele Segato

09/11/2018, 7:59 PM
I'll play with this a little... be right back
I had removed the
JsonValue
interface to let me use any name for the value.. So now when I try to use a lambda instead to provide the value through the reified inline function I get:
Illegal usage of inline-parameter 'jsonValue' in 'public inline fun <reified E : Enum<E>> createJsonConverter(jsonValue: (E) -> String): MyConverter<E> defined in YadaYada.kt'. Add 'noinline' modifier to the parameter declaration
inline fun <reified E: Enum<E>> createJsonConverter(jsonValue: (E) -> String): MyConverter<E> {
    return MyConverter<E>(E::class, enumValues<E>(), jsonValue)
}
the error on the last parameter
it's no big deal, code is already far better, this is more to learn then anything
a

Andreas Sinz

09/11/2018, 8:16 PM
@Daniele Segato can you post your
JsonValueEnum
?
d

Daniele Segato

09/11/2018, 8:46 PM
I removed it i'm trying without šŸ™‚ but it was just:
interface JsonValueEnum {
   val jsonValue: String
}
a

Andreas Sinz

09/11/2018, 8:47 PM
enum class SomeType(override val jsonValue: String): JsonValueEnum {
   // X = enum name, a = value in JSON
   X("a"),
   Y("b"),
   ;
    
    val GSON_TYPE_ADAPTER = createAdapter<SomeType>()
}
    
class MyAdapter<E: JsonValueEnum>(val values: () -> Array<E>) : TypeAdapter<E?>() {
    override fun write(output: JsonWriter?, value: E?) {
        if (value != null) output?.jsonValue(value.jsonValue) else output?.nullValue()
    }

    override fun read(input: JsonReader?): E? {
        val strValue = input?.nextString()
        return if (strValue != null) convertFromJsonValue(strValue) else null
    }
        
    private fun convertFromJsonValue(jsonValue: String): E {
        return try {
            // what should I put here?
            values().first { it.jsonValue == jsonValue }
        } catch (e: NoSuchElementException) {
            throw NoSuchElementException("Cannot find Enum-Value for jsonValue $jsonValue")
        }
    }
}

inline fun <reified E> createAdapter() where E: JsonValueEnum, E: Enum<E> = MyAdapter { enumValues<E>() }
d

Daniele Segato

09/11/2018, 8:49 PM
I can do it with the interface, I was trying to do without it.. using a lambda to provide what the interface now provide šŸ™‚
so that i can "attach" the converter to any enum even if they don't have the interface (as long as I have a string somewhere connected to the enum that i can use)
inline fun <reified E: Enum<E>> createAdapter(jsonValue: (E) -> String) = MyAdapter<E>(enumValues<E>(), jsonValue)
apparently this:
jsonValue: (E) -> String
can't be passed to the adapter in an inline function šŸ™‚
ah wait, i need the
noinline
only on the argument
let me see what this imply
a

Andreas Sinz

09/11/2018, 8:58 PM
whats the error message?
d

Daniele Segato

09/11/2018, 9:00 PM
I wrote it above:
Illegal usage of inline-parameter 'jsonValue' in 'public inline fun <reified E : Enum<E>> createJsonConverter(jsonValue: (E) -> String): MyConverter<E> defined in YadaYada.kt'. Add 'noinline' modifier to the parameter declaration
ohhh I see what it does (reading about noinline) .... this makes sense... I'm reading this: https://android.jlelse.eu/inline-noinline-crossinline-what-do-they-mean-b13f48e113c2 what's the behavior when I don't specify any of
inline
,
noinline
,
crossinline
?
Maybe i should ask this in channel, it's another question
a

Andreas Sinz

09/11/2018, 9:33 PM
if you wanna go the reusable way, why not start with the most generic way possible and then create more concrete stuff as needed?
interface JsonValueEnum {
    val jsonValue: String
}

class MyAdapter<E>(val serialize: (E) -> String, val deserialize: (String) -> E) : TypeAdapter<E?>() {
    override fun write(output: JsonWriter?, value: E?) {
        if (value != null) output?.jsonValue(serialize(value)) else output?.nullValue()
    }

    override fun read(input: JsonReader?): E? {
        val strValue = input?.nextString()
        return if (strValue != null) deserialize(strValue) else null
    }
}

inline fun <reified E: Enum<E>> createEnumAdapter(noinline serialize: (E) -> String, noinline  match: (E, String) -> Boolean): MyAdapter<E> {
    return MyAdapter(serialize, { jsonValue -> enumValues<E>().first { match(it, jsonValue) } })
}

inline fun <reified E> createJsonValueEnumAdapter(): MyAdapter<E> where E: Enum<E>, E: JsonValueEnum {
    return createEnumAdapter<E>({ it.jsonValue }, { enumValue, stringValue -> enumValue.jsonValue == stringValue })
}
d

Daniele Segato

09/12/2018, 5:42 PM
Good suggestion!