parceler icon indicating copy to clipboard operation
parceler copied to clipboard

Multiple ParcelConstructor-annotated methods in Kotlin.

Open jlaunonen opened this issue 8 years ago • 4 comments

Somewhat regarding to #275 comment 3 ... This may be more of just a documentation than an issue with Parceler, but here we go...

Kotlin allows parameters to have default values in function and constructor definitions. This (among other things) allows compact definition of data classes (think them as POJO's). e.g.

data class TimeStamp(val value: Long = System.currentTimeMillis())

As Parceler does the handling of serialization and deserialization of view models and whatnot keeping programmers sane in Android-world, consider following (bad) example:

@Parcel(Parcel.Serialization.BEAN)
data class MyScreenViewModel @ParcelConstructor constructor(
    val title: ObservableField<String> = ObservableField(),
    val subTitle: ObservableField<String> = ObservableField()
) {
    // This provided here only as some kind of additional non-default behaviour.
    // Has nothing to do with the issue otherwise and could be left away.
    constructor(title: String) : this(
        title = ObservableField(title)
    )
}

One can now create instances of that class in few ways:

val vmDefault = MyScreenViewModel() // calls the primary ctor with all defaults.
   .apply { title.set("Options") } // syntactic kotlin-sugar.
val vmSpecific = MyScreenViewModel("Options") // calls secondary ctor; not an issue.

However, this doesn't work with Parceler (at least 1.1.8...1.1.9) due error Parceler: Too many @ParcelConstructor annotated constructors found..

Insides

It seems that if all arguments of a data class have default parameters, kotlinc will generate a default no-argument constructor with the @ParcelConstructor annotation and the compilation will fail. If there is at least one argument without default value, compilation seems to succeed even though there are multiple of annotated constructors.

Internal decompilation shows, that there is at least one synthetic method added to data classes that handles the optionality of the parameters case by case (var3 is a bitmask):

data class Test @ParcelConstructor constructor(val i: Int, val s: String? = null)
->
// expected
@ParcelConstructor public Test(int i, @Nullable String s)  
// synthetic, causes no error.
@ParcelConstructor public Test(int var1, String var2,
    int var3, DefaultConstructorMarker var4)

If all arguments have defaults, there will be default constructor too:

data class Test @ParcelConstructor constructor(val i: Int = 4, val s: String? = null)
->
// extra
@ParcelConstructor public Test() : this(4, null, 3, null)
// full
@ParcelConstructor public Test(int i, @Nullable String s)  
// synthetic
@ParcelConstructor public Test(int var1, String var2,
    int var3, DefaultConstructorMarker var4)

The synthetic constructor always calls full/expected constructor. The default constructor calls synthetic constructor.

Workarounds

1

The issue can be worked around by having no defaults in primary constructor, although being a bit more verbose (but still much shorter than its java-version):

data class MyScreenVm @ParcelConstructor constructor(
    val title: ObservableField<String>,
    val subTitle: ObservableField<String>
) {
   constructor() : this(
      title = ObservableField(),
      subTitle = ObservableField()
   )
}

Here the primary constructor has no defaults, thus no synthetic method for defaults is generated, no default constructor is generated (but the explicit one exists without conflicting annotation), and the annotation is on only one constructor.

2

Other workaround is to declare the default constructor by hand and calling the primary constructor with at least one parameter:

data class MyScreenVm @ParcelConstructor constructor(
    val title: ObservableField<String> = ObservableField(),
    val subTitle: ObservableField<String> = ObservableField()
) {
    constructor() : this(title = ObservableField())
}

The default of title is kind of declared twice now, but it forces this to call the primary (or rather the synthetic) constructor and makes the default no-arg constructor to not have conflicting annotation.

jlaunonen avatar Aug 15 '17 14:08 jlaunonen

Is there any workaround made by parceler library side ?

This problem is very bad to using kotlin with data classes and default parameter values.

okarakose avatar Dec 07 '17 19:12 okarakose

What would you suggest @okarakose?

johncarl81 avatar Dec 31 '17 17:12 johncarl81

@johncarl81 please put some example on kotlin.

monowar1993 avatar Mar 14 '18 19:03 monowar1993

@monowar1993, all the documentation (readme, website) is fork-friendly. A pull request with some appropriate Kotlin examples would be much appreciated.

johncarl81 avatar Mar 16 '18 20:03 johncarl81