Sådan gemmer du Android View-tilstand i Kotlin

¯ \ _ (ツ) _ / ¯

Denne artikel er inspireret af den følgende store artikel, som mange Android-devs sandsynligvis har set før:

Ansvarsfraskrivelse: dette er en rigtig historie, der skete for mig, men det kan være anderledes for andre mennesker.

Lad os sige, at du følger denne artikel trin for trin og opretter en visning, der har standard (sammenbrud) og udvidede tilstande.

standard (sammenbrudt) tilstandudvidet tilstand

Du har sandsynligvis et felt, der har en værdi for at holde styr på den aktuelle tilstand:

privat boolsk er udvidet;

Og for at få visningen til at overleve skærmrotation skriver du sandsynligvis noget lignende:

@Override
public Parcelable onSaveInstanceState () {
    SavedState gemtState = nyt SavedState (super.onSaveInstanceState ());
    savedState.isExpanded = erExpanded;
    return gemte stat;
}

@Override
public void onRestoreInstanceState (Parcelable state) {
    if (angiv forekomst af SavedState) {
        SavedState gemtState = (SavedState) tilstand;
        super.onRestoreInstanceState (savedState.getSuperState ());
        isExpanded = gemtState.isExpanded;
    } andet {
        super.onRestoreInstanceState (state);
    }
}

statisk klasse SavedState udvider BaseSavedState {

    boolsk er udvidet;

    SavedState (Pakkeskilde) {
        super (kilde);
        isExpanded = source.readByte ()! = 0;
    }

    SavedState (Parcelable superState) {
        super (superstat);
    }

    @Override
    public void writeToParcel (Pakke ud, int-flag) {
        super.writeToParcel (ud, flag);
        out.writeByte ((byte) (er udvidet? 1: 0));
    }

    offentlig endelig statisk Opretter  CREATOR = ny Opretter  () {

        @Override
        public SavedState createFromParcel (Pakkeskilde) {
            returner nye SavedState (kilde);
        }

        public SavedState [] newArray (int-størrelse) {
            returner nye SavedState [størrelse];
        }
    };
}
Bemærk: Hvis en del af koden ovenfor ikke er klar for dig, bedes du henvise til artiklen, der er placeret øverst

Alt fungerer og Java, men det er tid til at komme videre og skrive kode i Kotlin. Lad os sige, at du har besluttet at konvertere din visning og det brugte indbyggingsværktøj.

ja, denne

Du laver nogle røde linjer, fordi konverter ikke er perfekt, og nu er dit felt alt i Kotlin:

privat var isExpanded = falsk

Og kode til at gemme og gendanne stater ser også temmelig Kotlin ud:

offentlig tilsidesættelse af sjov onSaveInstanceState (): Pakket? {
    val savedState = SavedState (super.onSaveInstanceState ())
    savedState.isExpanded = erExpanded
    return gemte stat
}

offentlig tilsidesættelse af sjov onRestoreInstanceState (tilstand: Parcelable) {
    hvis (staten er SavedState) {
        super.onRestoreInstanceState (state.superState)
        isExpanded = state.isExpanded
    } andet {
        super.onRestoreInstanceState (stat)
    }
}

intern klasse SavedState: View.BaseSavedState {

    var er udvidet: Boolsk = falsk

    konstruktør (kilde: Pakke): super (kilde) {
        isExpanded = source.readByte (). toInt ()! = 0
    }

    konstruktør (superState: Parcelable): super (superState)

    tilsidesætte sjovt skriveToParcel (ud: Pakke, flag: Int) {
        super.writeToParcel (ud, flag)
        out.writeByte ((hvis (er udvidet) 1 andet 0) .toByte ())
    }

    val CREATOR: Parcelable.Creator  = objekt: Parcelable.Creator  {

        tilsidesætte sjov createFromParcel (kilde: Parcel): SavedState {
            returner SavedState (kilde)
        }

        tilsidesætte sjove newArray (størrelse: Int): Array  {
            return arrayOfNulls (størrelse)
        }
    }
}

Du har kørt din kode under forudsætning af, at intet er ændret, men så får du dette grimme nedbrud:

java.lang.RuntimeException: Pakke android.os.Parcel @ : Fjernelse af ukendt type kode  ved forskydning 

Så går du på google og forsøger at finde årsagen, men alle stackoverflow-svar fortæller, at du skriver en type i pakke, men prøver at læse en anden type (eksempel 1, eksempel 2 og mere), som ikke er din sag.

Roden til problemet her er Parcelable.Creator klasse. Hvis du ser på Parcelable.Creator-dokumenter, gør du følgende:

Grænseflade, der skal implementeres og leveres som et offentligt CREATOR-felt, der genererer forekomster af din Parcelable-klasse fra en pakke.

Så ser du ind i koden og ser

val CREATOR: Parcelable.Creator  = ...

CREATOR er et offentligt felt i Kotlin… ikke i Java.
Android Studio har et meget nyttigt værktøj til at inspicere, hvordan Kotlin bytecode er repræsenteret i Java. Værktøjet findes i “Værktøjer → Kotlin → Vis Kotlin-bykode”. Dette genererer bytecode, som du derefter kan dekompilere til Java. Når du har gjort det, skal du se:

offentlig statisk slutklasse SavedState udvider BaseSavedState {
   @NotNull
   privat endelig Skaberskaber;
   @NotNull
   offentlig endelig Creator getCREATOR () {
      returner dette. SKABER;
   }
...
}

Offentligt felt i Kotlin svarer til det private felt med en offentlig getter i Java. Dette er dybest set, hvorfor du får nedbruddet i første omgang. For at ordne det, skal du bruge @JvmField-kommentar, der ifølge dokumenter:

Pålægger Kotlin-compileren ikke at generere bogstaver / sættere for denne egenskab og udsætte den som et felt.

Ser du på dekompileret Java-kode efter tilføjelse af @JvmField-annotation kunne du se følgende:

offentlig statisk slutklasse SavedState udvider BaseSavedState {
   @JvmField
   @NotNull
   offentlig endelig Skaberskaber;
...
}

Det er et offentligt felt nu, men der er en ting mere. Hvis du kigger på Pakkbare dokumenter, vil du se:

Interface til klasser, hvis forekomster kan skrives til og gendannes fra en pakke. Klasser, der implementerer Parcelable-grænsefladen, skal også have et ikke-nullstatisk felt kaldet CREATOR af en type, der implementerer Parcelable.Creator-grænsefladen.

Feltet er endnu ikke statisk, så hvis kør koden, vil det stadig gå ned med en følgende stacktrace:

android.os.BadParcelableException: Parcelable-protokol kræver, at CREATOR-objektet er statisk i klassen

For at opnå dette kunne du bruge Kotlins ledsagerobjekt. Se på dokumenterne, så finder du det:

et ledsagerobjekt initialiseres, når den tilsvarende klasse indlæses (løses), der matcher semantikken i en Java-statisk initialisator

Ombrydes det omkring CREATOR-feltet, og det er det!

ledsagerobjekt {
    @JvmField
    val CREATOR = objekt: Parcelable.Creator  {
        tilsidesætte sjov createFromParcel (kilde: Parcel): SavedState {
            returner SavedState (kilde)
        }

        tilsidesætte sjove newArray (størrelse: Int): Array  {
            return arrayOfNulls (størrelse)
        }
    }
}

Du får ikke crash på dette sted, visningen gemmer en gendannelsestilstand korrekt. Hvis du ser på dekompileret Java-kode, vil du se, at feltet er offentligt og statisk, hvilket er nøjagtigt, hvad Parcelable interface har brug for.

offentlig statisk slutklasse SavedState udvider BaseSavedState {
   @JvmField
   @NotNull
   offentlig statisk endelig Opretter CREATOR = ...
...
}

Stor tak til Abhi https://github.com/abhiin1947 for at have vist mig @JvmField og Olivia https://github.com/oliviaperryman for at grave dybt ned i dette problem