Posts mit dem Label onRestoreInstanceState werden angezeigt. Alle Posts anzeigen
Posts mit dem Label onRestoreInstanceState werden angezeigt. Alle Posts anzeigen

7. April 2013

Android: Komplexe Objekte in Bundle speichern

In Android ist es nötig, gewisse Zustände einer Activity oder eines Fragments zu speichern und wiederherzustellen, insofern die Werte nicht verloren gehen sollen, wenn die Activity bzw. das Fragment zerstört und neu gestartet wird, wie es z.B. bei einem Orientierungswechsel des Geräts geschieht. Die prinzipielle Vorgehensweise zum Sichern solcher Werte wurde bereits in dem Post "Die Activity-Methoden onSaveInstanceState und onRestoreInstanceState" für Activities und in dem Post "Die Fragment-Methoden onSaveInstanceState und onActivityCreated" für Fragments erklärt.

Meistens handelt es sich bei den Werten, die dabei in dem Bundle-Objekt gespeichert werden um primitive Datentypen. Hierfür bietet die Bundle-Klasse jeweils Methoden um Werte hinzuzufügen, bzw. später wieder auszulesen. Es besteht jedoch darüber hinaus auch die Möglichkeit, komplexe Objekte zu sichern, indem die Methode putSerializable(String key, Serializable value):void zum Ablegen eines Objekts in dem entsprechenden Bundle, bzw. die Methode getSerializable(String key):Serializable zum referenzieren eines zuvor gespeicherten Objekts aus dem Bundle, genutzt wird. Das zu sichernde Objekt muss demnach die das Interface java.io.Serializable implementieren.

Sollen allerdings Objektinstanzen von Klassen gespeichert werden, die dieses Interface nicht implementieren und die nicht angepasst werden können, weil sie beispielsweise Klassen der Java-API oder eines Frameworks sind, steht man vor dem Problem, dass solche Klassen nicht ohne Weiteres an ein Bundle übergeben werden können. Eine Möglichkeit, wie dieses Problem umgangen werden kann, soll nun im Folgenden aufgezeigt werden.

Die Lösung besteht darin, eine Wrapper-Klasse zu schreiben, die das Serializable-Interface implementiert und eine Instanz des zu sichernden Objekttyps aufnimmt. Dabei sind grundsätzlich zwei Ansätze denkbar: Entweder man entscheidet sich für einen sehr einfachen Wrapper, der lediglich eine Getter- und eine Setter-Methode auf das gekapselte Objekt anbietet, oder man setzt auf eine komplexere, aber eleganter zu verwendende Implementierung, die zudem als Proxy fungiert, indem sie neben dem Serializable-Interface auch die Schnittstelle des zu sichernden Objekttyps implementiert und alle Methodenaufrufe an das gekapselte Objekt weiterleitet. Die hat den Vorteil, dass sich Instanzen dieser Klasse exakt wie das Original handhaben lassen. Eventuell bereits vorhandener Code muss auf diese Weise kaum angepasst werden. Auf eine mögliche Implementierung dieser Variante soll nun näher eingegangen werden. Als Code-Beispiel ist im Folgenden ein Wrapper für das Interface java.util.List, das als Schnittstelle für alle Listenimplementierungen der Java-Standardbibliothek dient, zu sehen. Als Szenario für die Sicherung einer solchen Datenstruktur wäre beispielsweise ein Listenadapter denkbar, der Suchergebnisse in einer ListView darstellt. Wenn bei einem Orientierungswechsel der entsprechenden Activity der zeitaufwendige Netzwerkzugriff der Suchanfrage nicht erneut durchgeführt werden soll, muss die zugrunde liegende Datenstruktur zwischengespeichert werden.
 public class SerializableList<Type> implements List<Type>, Serializable {  
   
     private static final long serialVersionUID = 1L;  
   
     private List<Type> list;  
   
     public SerializableList(final List<Type> list) {  
         this.list = list;  
     }  
   
     public final boolean add(final Type object) {  
         return list.add(object);  
     }  
   
     public final void add(final int location, final Type object) {  
         list.add(location, object);  
     }  
   
     public final boolean addAll(final Collection<? extends Type> collection) {  
         return list.addAll(collection);  
     }  
   
     public final boolean addAll(final int index,  
             final Collection<? extends Type> collection) {  
         return list.addAll(index, collection);  
     }  
   
     public final void clear() {  
         list.clear();  
     }  
   
     public final boolean contains(final Object object) {  
         return list.contains(object);  
     }  
   
     public final boolean containsAll(final Collection<?> collection) {  
         return list.containsAll(collection);  
     }  
   
     public final Type get(final int location) {  
         return list.get(location);  
     }  
   
     public final int indexOf(final Object object) {  
         return list.indexOf(object);  
     }  
   
     public final boolean isEmpty() {  
         return list.isEmpty();  
     }  
   
     public final Iterator<Type> iterator() {  
         return list.iterator();  
     }  
   
     public final int lastIndexOf(final Object object) {  
         return list.lastIndexOf(object);  
     }  
   
     public final ListIterator<Type> listIterator() {  
         return list.listIterator();  
     }  
   
     public final ListIterator<Type> listIterator(final int location) {  
         return list.listIterator(location);  
     }  
   
     public final Type remove(final int location) {  
         return list.remove(location);  
     }  
   
     public final boolean remove(final Object object) {  
         return list.remove(object);  
     }  
   
     public final boolean removeAll(final Collection<?> collection) {  
         return list.removeAll(collection);  
     }  
   
     public final boolean retainAll(final Collection<?> collection) {  
         return list.retainAll(collection);  
     }  
   
     public final Type set(final int location, final Type object) {  
         return list.set(location, object);  
     }  
   
     public final int size() {  
         return list.size();  
     }  
   
     public final List<Type> subList(final int start, final int end) {  
         return list.subList(start, end);  
     }  
   
     public final Object[] toArray() {  
         return list.toArray();  
     }  
   
     public final <T> T[] toArray(final T[] array) {  
         return list.toArray(array);  
     }  
   
     @Override  
     public final int hashCode() {  
         final int prime = 31;  
         int result = 1;  
         result = prime * result + ((list == null) ? 0 : list.hashCode());  
         return result;  
     }  
   
     @Override  
     public final boolean equals(final Object obj) {  
         if (this == obj)  
             return true;  
         if (obj == null)  
             return false;  
         if (getClass() != obj.getClass())  
             return false;  
         SerializableList<?> other = (SerializableList<?>) obj;  
         if (list == null) {  
             if (other.list != null)  
                 return false;  
         } else if (!list.equals(other.list))  
             return false;  
         return true;  
     }  
   
 }  
Die Klasse implementiert sowohl das Interface java.io.Serializable, als auch das Interface java.util.List. Außerdem handelt es sich um eine generische Klasse, die mit dem Typ der Objekte, die die gekapselte Liste aufnehmen soll, typisiert wird. Über den Konstruktor wird diese Liste übergeben und in dem entsprechenden Attribut gespeichert. Dieses wird dazu verwendet, um alle Aufrufe der Methoden, die in dem List-Interface definiert sind, unverändert an die gekapselte Liste weiter zu leiten. Darüber hinaus wurde auch jeweils eine Implementierung der hashCode- bzw. equals-Methode hinzugefügt, damit sich Klasseninstanzen tatsächlich so verhalten, wie man es von einer Liste erwarten würde.

Das Abspeichern der oben gezeigten Listenimplementierung in einem Bundle könnte beispielsweise wie folgt aussehen:
 public final void onSaveInstanceState(final Bundle outState) {  
   SerializableList<String> serializableList = new SerializableList<String>(new ArrayList<String>());  
   serializableList.add("elem1");  
   serializableList.add("elem2");  
   outState.putSerializable("list", serializableList);  
 }  
Um die, auf diese Weise im Bundle gespeicherte, Liste anschließend wiederherzustellen, sind dann folgende Zeilen nötig:
 public final void onRestoreInstanceState(final Bundle savedInstanceState) {  
   SerializableList<String> serializableList = (SerializableList<String>) savedInstanceState.getSerializable("list");  
 }  
Dabei ist ein Cast vom Interface Serializable auf die Serializable-List implementierung nötig.

9. Februar 2013

Android: Die Activity-Methoden onSaveInstanceState und onRestoreInstanceState

Standardmäßig verlieren die Activities einer Android-App temporäre Werte, wie Eingaben in Textfelder, die Auswahl von Checkboxen oder ähnliches, wenn sie neu erzeugt werden. Dies kommt beispielsweise vor, wenn vom Portrait- in den Landscape-Modus gewechselt wird oder umgekehrt und ist in so einem Fall oft unerwünscht. Deshalb bietet die Android-API die beiden Methoden onSaveInstanceState(Bundle outState): void und onRestoreInstanceState(Bundle savedInstanceState): void an, die es ermöglichen, zu erhaltene Zustände zwischenzuspeichern und anschließend wiederherzustellen. Die beiden Methoden werden dabei, dem Activity-Lifecycle entsprechend automatisch aufgerufen. Das folgende kurze Beispiel soll die Verwendung der beiden Methoden demonstrieren.

Der Activity-Lifecycle (Bildquelle: www.developer.android.com, Klicken zum Vergrößern)

Das folgende XML Layout File definiert die Oberfläche einer Activity, die ein Textfeld enthält. Ist dort ein Text eingetragen, geht dieser verloren, wenn vom Portrait- in den Landscape-Modus gewechselt wird, oder umgekehrt. Dieser unerwünschte Effekt soll im Folgenden behoben werden.

 <?xml version="1.0" encoding="utf-8">   
  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   
   android:orientation="vertical"   
   android:layout_width="fill_parent"   
   android:layout_height="fill_parent" >   
     
   <EditText   
    android:id="@+id/edit"   
    android:layout_width="fill_parent"   
    android:layout_height="wrap_content" >   
     
  </LinearLayout>   
Der Code der dazugehörigen Activity, inklusive der beiden oben genannten Methoden zum Speichern und Wiederherstellen des eingegebenen Textes, sieht dabei folgendermaßen aus:
 public class ExampleActivity extends Activity {  
   
   EditText edit;  
   
   @Override  
   public void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     setContentView(R.layout.main);  
      
     edit = (EditText)findViewById(R.id.edit);  
   }  
   
   @Override  
   public void onSaveInstanceState(Bundle outState) {  
    super.onSaveInstanceState(outState);  
    outState.putString("text", (String) text.getText());  
   }  
   
   @Override  
   public void onRestoreInstanceState(Bundle savedInstanceState) {  
    super.onRestoreInstanceState(savedInstanceState);  
    text.setText(savedInstanceState.getString("text"));  
   }  
    
 }  
Beim Erzeugen der Activity durch die onCreate-Methode wird das, im XML-File definierte, Textfeld zunächst anhand seiner ID referenziert. Bevor die Activity, beispielsweise durch ein Rotieren des Bildschirms, zerstört wird, wird automatisch die onSaveInstanceState-Methode aufgerufen, in der alle zu sichernden Werte zusammen mit einem String als Key in einem Bundle abgelegt werden können, das als Parameter übergeben wird. Das selbe Bundle wird dann an die onRestoreInstanceState-Methode übergeben, sobald die Activity neu erzeugt wurde. Die gespeicherten Werte können dann über den zuvor angegebenen Schlüssel aus dem Bundle gelesen und zur Wiederherstellung des vorherigen Zustandes verwendet werden.

Nachtrag vom 20. Februar 2013: Unter diesem Post ist von nun an die analoge Vorgehensweise zur Zwischenspeicherung von Zuständen in Fragments beschrieben.