So speichern Sie den Android View-Status in Kotlin

¯ \ _ (ツ) _ / ¯

Dieser Artikel ist inspiriert von dem folgenden großartigen Artikel, den viele Android-Entwickler wahrscheinlich schon einmal gesehen haben:

Haftungsausschluss: Dies ist eine echte Geschichte, die mir passiert ist, aber für andere Menschen könnte sie anders sein.

Nehmen wir an, Sie folgen diesem Artikel Schritt für Schritt und erstellen eine Ansicht mit Standardstatus (minimiert) und erweitertem Status.

Standardzustand (reduziert)erweiterter Zustand

Sie haben wahrscheinlich ein Feld, das einen Wert enthält, um den aktuellen Status zu verfolgen:

private boolean isExpanded;

Und um die Drehung des Bildschirms zu überstehen, schreiben Sie wahrscheinlich Folgendes:

@Override
public Parcelable onSaveInstanceState () {
    SavedState savedState = new SavedState (super.onSaveInstanceState ());
    savedState.isExpanded = isExpanded;
    return savedState;
}

@Override
public void onRestoreInstanceState (Parcelable state) {
    if (state instanceof SavedState) {
        SavedState savedState = (SavedState) state;
        super.onRestoreInstanceState (savedState.getSuperState ());
        isExpanded = savedState.isExpanded;
    } else {
        super.onRestoreInstanceState (state);
    }
}

statische Klasse SavedState erweitert BaseSavedState {

    boolean isExpanded;

    SavedState (Paketquelle) {
        super (Quelle);
        isExpanded = source.readByte ()! = 0;
    }

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

    @Override
    public void writeToParcel (Paket aus, int flags) {
        super.writeToParcel (out, flags);
        out.writeByte ((byte) (isExpanded? 1: 0));
    }

    public final static Creator  CREATOR = neuer Creator  () {

        @Override
        public SavedState createFromParcel (Paketquelle) {
            neuen SavedState (Quelle) zurückgeben;
        }

        public SavedState [] newArray (int size) {
            return new SavedState [Größe];
        }
    };
}
Hinweis: Wenn Ihnen ein Teil des obigen Codes nicht klar ist, lesen Sie bitte den Artikel oben

Alles funktioniert und Java, aber es ist an der Zeit, in Kotlin voranzukommen und Code zu schreiben. Angenommen, Sie haben beschlossen, Ihre Ansicht zu konvertieren und das integrierte Tool zu verwenden.

Ja, das hier

Sie korrigieren einige rote Linien, weil der Konverter nicht perfekt ist und Ihr Feld nun vollständig in Kotlin ist:

private var isExpanded = false

Und Code zum Speichern und Wiederherstellen von Zuständen sieht auch ziemlich gut aus:

Spaß beim Überschreiben durch die Öffentlichkeit onSaveInstanceState (): Parcelable? {
    val savedState = SavedState (super.onSaveInstanceState ())
    savedState.isExpanded = isExpanded
    geben Sie savedState zurück
}

public override fun onRestoreInstanceState (Bundesland: Parcelable) {
    if (state is SavedState) {
        super.onRestoreInstanceState (state.superState)
        isExpanded = state.isExpanded
    } else {
        super.onRestoreInstanceState (state)
    }
}

interne Klasse SavedState: View.BaseSavedState {

    var isExpanded: Boolean = false

    Konstruktor (Quelle: Paket): super (Quelle) {
        isExpanded = source.readByte (). toInt ()! = 0
    }

    Konstruktor (superState: Parcelable): super (superState)

    überschreibe fun writeToParcel (out: Parcel, flags: Int) {
        super.writeToParcel (out, flags)
        out.writeByte ((if (isExpanded) 1 else 0) .toByte ())
    }

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

        Spaß überschreiben createFromParcel (Quelle: Parcel): SavedState {
            SavedState (Quelle) zurückgeben
        }

        Überschreibe Spaß newArray (Größe: Int): Array  {
            Rückgabe von arrayOfNulls (Größe)
        }
    }
}

Sie führen Ihren Code unter der Annahme aus, dass sich nichts geändert hat, aber dann tritt dieser Absturz auf:

java.lang.RuntimeException: Parcel android.os.Parcel @ : Aufheben der Zuordnung des unbekannten Typencodes  am Offset 

Dann gehen Sie zu Google, um den Grund zu finden, aber alle Stackoverflow-Antworten besagen, dass Sie einen Typ in ein Paket schreiben, aber versuchen, einen anderen Typ zu lesen (Beispiel 1, Beispiel 2 und mehr), was nicht Ihr Fall ist.

Die Wurzel des Problems ist hier die Parcelable.Creator-Klasse. Wenn Sie sich Parcelable.Creator-Dokumente ansehen, werden Sie Folgendes sehen:

Schnittstelle, die implementiert und als öffentliches Feld CREATOR bereitgestellt werden muss, das Instanzen Ihrer Parcelable-Klasse aus einem Parcel generiert.

Dann schaust du in den Code und siehst

val CREATOR: Parcelable.Creator  = ...

CREATOR ist ein öffentliches Feld in Kotlin… nicht in Java.
Android Studio hat ein sehr nützliches Tool, um zu überprüfen, wie Kotlin-Bytecode in Java dargestellt wird. Das Tool befindet sich unter „Tools → Kotlin → Show Kotlin Bytecode“. Dadurch wird ein Bytecode generiert, den Sie in Java dekompilieren können. Nachdem Sie das getan haben, sollten Sie sehen:

public static final class SavedState erweitert BaseSavedState {
   @Nicht null
   privater letzter Schöpfer SCHÖPFER;
   @Nicht null
   public final Creator getCREATOR () {
      gib das zurück.CREATOR;
   }
...
}

Das öffentliche Feld in Kotlin entspricht dem privaten Feld mit einem öffentlichen Getter in Java. Dies ist im Grunde, warum Sie den Absturz in erster Linie bekommen. Um dies zu beheben, müssen Sie die @ JvmField-Annotation verwenden, die den folgenden Dokumenten entspricht:

Weist den Kotlin-Compiler an, keine Getters / Setter für diese Eigenschaft zu generieren und sie als Feld verfügbar zu machen.

Wenn Sie den dekompilierten Java-Code nach dem Hinzufügen der Annotation @JvmField betrachten, können Sie Folgendes feststellen:

public static final class SavedState erweitert BaseSavedState {
   @JvmField
   @Nicht null
   öffentlicher finaler Creator CREATOR;
...
}

Es ist jetzt ein öffentliches Gebiet, aber es gibt noch eine Sache. Wenn Sie sich Paketdokumente ansehen, werden Sie sehen:

Schnittstelle für Klassen, deren Instanzen in ein Paket geschrieben und aus diesem wiederhergestellt werden können. Klassen, die die Parcelable-Schnittstelle implementieren, müssen auch ein statisches Nicht-Null-Feld mit dem Namen CREATOR haben, dessen Typ die Parcelable.Creator-Schnittstelle implementiert.

Das Feld ist noch nicht statisch. Wenn Sie also den Code ausführen, stürzt das Feld mit folgendem Stacktrace ab:

android.os.BadParcelableException: Für das Parcelable-Protokoll muss das CREATOR-Objekt in der Klasse statisch sein

Um dies zu erreichen, können Sie das Begleitobjekt von Kotlin verwenden. Schauen Sie sich die Dokumente an und Sie werden feststellen, dass:

Ein Companion-Objekt wird initialisiert, wenn die entsprechende Klasse geladen (aufgelöst) wird. Dies entspricht der Semantik eines statischen Java-Initialisierers

Wickeln Sie es um das CREATOR-Feld und fertig!

Begleitobjekt {
    @JvmField
    val CREATOR = Objekt: Parcelable.Creator  {
        Spaß überschreiben createFromParcel (Quelle: Parcel): SavedState {
            SavedState (Quelle) zurückgeben
        }

        Überschreibe Spaß newArray (Größe: Int): Array  {
            Rückgabe von arrayOfNulls (Größe)
        }
    }
}

An dieser Stelle kommt es nicht zu Abstürzen. Die Ansicht speichert den Wiederherstellungsstatus korrekt. Wenn Sie sich dekompilierten Java-Code ansehen, werden Sie feststellen, dass das Feld öffentlich und statisch ist. Dies ist genau das, was die Parcelable-Schnittstelle benötigt.

public static final class SavedState erweitert BaseSavedState {
   @JvmField
   @Nicht null
   public static final Creator CREATOR = ...
...
}

Vielen Dank an Abhi https://github.com/abhiin1947, der mir @JvmField und Olivia https://github.com/oliviaperryman gezeigt hat, dass sie sich mit diesem Problem befasst haben