Stand: 26.06.2018

Natürlich müssen Sie Daten speichern und laden. Bei einem Spiel wollen Sie Spielstände/Punkte speichern, bei einem Adressbuch die Adressen, Sie wollen Fotos aufnehmen und Audioaufnahmen ablegen.

In Modul Daten I haben wir gelernt, wie wir Daten speichern, so dass sie von allen Activities gelesen werden können. Anlass war, dass wir gemerkt haben: wenn wir auch nur die Orientierung ändern, wird meine Activity neu gestartet und alle Instanzvariablen werden auf den Initalwert gesetzt.

Überlegen wir, in welchen Situationen Daten verschwinden können:

Mit einem globalen Singleton decken wir die ersten zwei Punkte ab.

Jetzt möchten wir die Daten auch dann noch behalten, wenn wir die App schließen und später wieder öffnen. Teilweise möchten wir Daten auch behalten, wenn wir die App deinstallieren.

Prinzipiell gibt es für Textinformationen drei "Stufen" der Speicherung mit zunehmender Komplexität (und Mächtigkeit):

Wir beschränken uns hier auf die ersten zwei Optionen.

9.1 Shared Preferences

API: SharedPreferences

Der Preferences-Mechanismus erlaubt es, einfach Attribut-Wert-Paare zu speichern, d.h. man definiert einen Schlüssel (String) für jeden Datenelement.

Ein Beispiel wäre eine Liste mit Spielständen (Punkte). Für jede/n Spieler/in muss man also eine Zahl speichern. Jede/r Spieler/in wird mit dem Spielernamen als Schlüssel repräsentiert.

Man kann sich das als Tabelle vorstellen:

Attribut Wert
Sally 2521
Harry 42
Lara 2001
Mark 8815

Für umfangreiche Informationen, wie sie z.B. in Listen oder Arrays vorliegen, ist diese Speichermethode nicht geeignet; in solchen Fällen nutzt man besser Dateien oder Datenbanken.

Der Preferences-Mechanismus ist zudem relativ langsam, da starke Konsistenzgarantien gegeben werden, d.h. es sollte auch nicht in Performance-kritischen Situationen genutzt werden (z.B. innerhalb eines Game-Loops).

9.1.1 SharedPreferences-Objekt

Die Tabelle wird in einem Objekt gespeichert. Man kann tatsächlich mehrere Preferences-Objekte nutzen. Wichtig ist, dass man diesen Objekten einen eindeutigen Namen gibt, der die Preferences auch von anderen Apps abgrenzt.

Wenn Ihre App "FooApp" heißt, könnte man "FooAppPrefs" als Namen nehmen. Mit diesem Befehl bekommen Sie ein entsprechendes Objekt bzw. lassen es herstellen, wenn es noch nicht existiert:

SharedPreferences prefs = getSharedPreferences("FooAppPrefs", Context.MODE_PRIVATE);  

Das Argument Context.MODE_PRIVATE bedeutet, dass nur die eigene App auf die Daten zugreifen darf.

Sie können den Namen des Preferences-Objekts auch halbautomatisch zusammensetzen lassen, indem Sie über das R-Objekt auf den Namen den App zugreifen:

String prefsName = getString(R.string.app_name) + "Prefs";
SharedPreferences prefs = getSharedPreferences(prefsName, Context.MODE_PRIVATE);  

9.1.2 Speichern

Zum Speichern müssen wir zunächst das Preferences-Objekt beziehen. Dieses Objekt selbst hat nicht die Fähigkeit, Änderungen vorzunehmen, das kann nur ein entsprechendes Editor-Objekt. Wir können einen solchen Editor über die Methode edit:

SharedPreferences prefs = getSharedPreferences("FooAppPrefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();

Ein Editor funktioniert so, dass Sie ihm erst eine Reihe von Änderungen mitgeben und dann mit apply sagen, dass diese Änderungen durchgeführt werden sollen. Für jede Änderung (Schreiben von Daten) geben wir den Schlüssel und den Wert an. Je nachdem, ob Sie einen String oder eine Zahl als Wert speichern möchten, müssen Sie die Schreibmethoden putString oder putInt verwenden:

editor.putString("mytext", "once upon a time");
editor.putInt("meaningoflife", 42);
editor.apply();

Für unser Beispiel mit den Spielständen:

editor.putInt("Sally", 2521);
editor.putInt("Harry", 42);
editor.putInt("Lara", 2001);
editor.putInt("Mark", 8815);
editor.apply();

Weitere Schreibmethoden sind:

9.1.3 Laden

Beim Laden von Daten greifen wir direkt auf das Preferences-Objekt zu:

SharedPreferences prefs = getSharedPreferences("FooAppPrefs", Context.MODE_PRIVATE);
String text = prefs.getString("mytext", "not found");
int num = prefs.getInt("meaningoflife", 0);

Der jeweils zweite Parameter gibt den Default-Wert an, der verwendet werden soll, wenn der Schlüssel nicht vorhanden ist.

Wieder unser Beispiel:

int sally = prefs.getInt("Sally", -1);
int harry = prefs.getInt("Harry", -1);
int lara = prefs.getInt("Lara", -1);
int mark = prefs.getInt("Mark", -1);

9.1.4 Löschen

Sie können auch einen Eintrag entfernen, d.h. sowohl Schlüssel als auch Wert werden aus der Tabelle gelöscht (man entfernt quasi eine Zeile). Da das Löschen eine Änderung ist, benötigen Sie wieder den Editor.

SharedPreferences prefs = getSharedPreferences("FooAppPrefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();

Welcher Eintrag gelöscht wird, bestimmt man durch Angabe des Schlüssels:

editor.remove("Mark");

Man kann auch alle Einträge, also die komplette Tabelle, löschen:

editor.clear();

Auch hier muss man die Änderung erst per Kommando in Kraft setzen:

editor.apply();

9.2 Dateien

Für den Umgang mit Dateien verwendet man dieselben Klassen wie in Java. Man unterscheidet zwischen internem Speicher und externem Speicher. Lesen Sie sorgfältig, was diese Begriffe bedeuten, denn die Bedeutung ist nicht für jeden intuitiv.

Interner Speicher bezieht sich auf einen Teil des Dateisystems, der nur für die App selbst zu erreichen ist. Das bedeutet, andere Apps können nicht darauf zugreifen und auch in einer File-Manager-App können Sie diese Dateien nicht sehen. Es bedeutet auch, dass beim Entfernen der App diese Dateien gelöscht werden. Wenn Sie also Daten wie Texte und Bilder speichern wollen, die später von anderen Apps gelesen werden sollen, dann ist der interne Speicher ungeeignet. Hier ist ein interessanter Artikel über den Internal Storage.

Externer Speicher ist ein Ort im Dateisystem, der theoretisch auch für andere Apps zugänglich ist. Hier kann man nochmal unterscheiden zwischen Teilen im Dateisystem, die zwar zugänglich sind, aber speziell für die App reserviert sind (und auch entfernt werden, wenn die App gelöscht wird), und zwischen wirklich öffentlichen (public) Verzeichnissen, die z.B. auch die Standardordner für Dokumente, Bilder und Musik. Außerdem kann sich externer Speicher auf echt "externe" Speichermeden beziehen wie etwa eine SD-Karte. Hier auch noch ein Artikel über den External Storage.

Wir beschränken uns hier auf den internen Speicher.

9.2.1 Klasse File

API: File

Die folgenden Ausführungen gelten zunächst mal ganz allgemein für Java-Programme, egal ob auf Android-Geräten oder auf anderen Rechnern.

Bevor wir zum Lesen/Schreiben kommen, sollten wir uns die Klasse File aus dem Paket java.io anschauen. Bislang haben wir unsere Dateinnamen und Pfade mit einem einfachen String repräsentiert. Die Klasse File erlaubt einen etwas eleganteren Umgang mit Pfaden. Wir erzeugen einfach mal zwei File-Objekte, indem wir jeweils einen Pfad als String übergeben. Das Beispiel bezieht sich auf einen traditionellen Desktop-Rechner:

File file1 = new File("C:/Foo/Baa/meineEinkaufsliste.txt"); // absolut
File file2 = new File("freunde.txt"); // relativ

Bei file1 sehen Sie einen absoluten Pfad, weil er von der "Wurzel" des Dateisystems (hier wäre das C:) bis zum Ziel alles auflistet. Ein relativer Pfad wie bei file2 geht von der aktuellen Position des Systems aus, i.d.R. das aktuelle Projektverzeichnis.

Weitere Beispiele für relative Pfade sind:

File file3 = new File("trash/feinde.txt"); // in einem Unterverz.
File file4 = new File("../chefs.txt"); // im übergeordneten Verz.

Bei diesen Beispielen "navigieren" wir, ausgehend vom aktuellen Verzeichnis, in ein Unterverzeichnis (file3) oder in das übergeordnete Verzeichnis (file4).

Pfade zusammensetzen

Häufig liegen Dateien nicht genau im Projektverzeichnis, sondern z.B. im Dokument-Verzeichnis des Benutzers oder ganz woanders. Sie können Pfade als String zusammensetzen, indem Sie je einen Schrägstrich (engl. slash) einbauen.

String pfad = "C:/users/kipp/Documents";
String dateiname = "foo.txt";
File file = new File(pfad + "/" + dateiname);

Dabei sollten Sie darauf achten, immer den Forward-Slash (/) zu verwenden, und nicht den Backward-Slash(). Der Forward-Slash funktioniert unter allen Betriebssystemen (Win, Mac, Linux), der Backward-Slash nur unter Windows.

Man dennoch ganz allgemein versuchen, den Slash zu vermeiden. Die Klasse File bietet einen Konstruktor an, wo Sie Pfad und Dateiname als Parameter übergeben und der komplette Pfade dann von Java gebaut wird:

File file = new File(pfad, dateiname);

Diese Variante ist die empfohlene.

Methoden von File

Einen Pfad in ein File-Objekt "einzupacken" ist für viele Klassen und Methoden notwendig, damit diese den Pfad nutzen können. Die Klasse File ist aber auch für sich genommen nützlich. So können Sie das Objekt befragen, z.B. ob die Datei existiert, ob es sich um Datei oder Verzeichnis handelt etc.

File file = new File("meineEinkaufsliste.txt");

// Jetzt die Befragung:

if (file.exists()) {
  Log.i("App", "Ich bin!");
}

if (file.isFile()) {
  Log.i("App", "Ich bin eine Datei!");
}

if (file.isDirectory()) {
  Log.i("App", "Ich bin ein Verzeichnis!");
}

Mit toString() können Sie auch jederzeit wieder den Pfad ansehen oder ausgeben, wobei Sie das häufig nicht explizit machen müssen, weil es automatisch aufgerufen wird, wie hier:

File file = new File("meineEinkaufsliste.txt");
Log.i("App", "Pfad: " + file);

Ansonsten können Sie noch einiges mehr mit der Klasse File anstellen, z.B. Dateien löschen, den Inhalt von Verzeichnissen untersuchen, Verzeichnisse erzeugen etc.

9.2.2 Klasse Scanner

API: Scanner

Die Klasse Scanner aus dem Paket java.util wurde speziell zum Einlesen von verschiedenen Datentypen (Zahlen, Texten etc.) gemacht. Wir beschäftigen uns hier mit Text in Form von Strings.

Ob ein Scanner seine Daten aus einer Datei oder aus dem Internet oder von der Tastatur des Users liest, ist übrigens egal. Der Scanner ist sehr allgemein gehalten. Zunächst mal gehen wir aber davon aus, dass wir eine Datei einlesen wollten.

Zunächst erzeugt man ein Scanner-Objekt, indem man ihm ein File-Objekt zum Öffnen übergibt.

File file = new File("secretStuff.txt");
Scanner scanner = new Scanner(file);

Das Scanner-Objekt funktioniert wie ein Cursor, den wir durch die Datei bewegen. Der Cursor springt zum Beispiel zeilenweise durch den Text und gibt dabei die übersprungene Zeile zurück.

Das Scanner-Objekt hat dafür die Methoden hasNextLine() (ist true, wenn es noch eine Zeile zum Überspringen gibt) und nextLine() (überspringt die nächste Zeile und gibt diese zurück).

Die Methoden lassen sich wie folgt in eine While-Schleife einbauen:

while (scanner.hasNextLine()) {
  String line = scanner.nextLine();
  Log.i("App", "> " + line);
}

Nach Verwendung des Scanners, sollte der "Datenstrom" geschlossen werden (engl. close), damit gebundene Ressourcen freigegeben werden. Außerdem kann es sein, dass Daten verloren gehen, wenn der Datenstrom nicht ordnungsgemäß geschlossen wird.

scanner.close();

Der Code wird in dieser Form nicht laufen, da der Compiler etwas über eine "Exception" erzählt und sich weigert, den Code zu compilieren. Schuld ist die Zeile new Scanner(file). Hier könnte es sein, dass das übergebene File nicht existiert - eine FileNotFoundException könnte geworfen werden. Wir bauen ein entsprechendes try-catch-Konstrukt ein:

try {
  File file = new File("secretStuff.txt");
  Scanner scanner = new Scanner(file);
  while (scanner.hasNextLine()) {
    String line = scanner.nextLine();
    Log.i("App", "> " + line);
  }
  scanner.close();
} catch (FileNotFoundException ex) {
  ex.printStackTrace();
}

Der Scanner ist sowohl in Java als auch in Android die Klasse der Wahl zum Einlesen von Daten.

9.2.3 Textdatei als Ressource [optional]

Wenn Sie Ihrer App eine große Anzahl an Infos in Form einer Textdatei mitgeben wollen, dann können Sie einfach eine Textdatei in einen Ressourcenordner namens raw deponieren.

Solche Dateien können Sie allerdings nur lesen und nicht neu beschreiben.

Das können Sie direkt in Android Studio machen, indem Sie den Ordner res markieren und mit Rechtsklick New > Directory wählen und ein Verzeichnis "raw" erstellen. Anschließen markieren Sie das neue Verzeichnis und wählen per Rechtsklick New > File aus, das Sie z.B. "data.txt" nennen.

Auf die Ressource greifen Sie über das R-Objekt zu:

R.raw.data

Um die Datei zu lesen, nutzen Sie einen Scanner, diesmal aber mit einem InputStream. Hier geben wir die Zeilen der Datei auf der Log-Konsole aus:

InputStream in = getResources().openRawResource(R.raw.data);
Scanner scanner = new Scanner(in);
while (scanner.hasNext()) {
    String line = scanner.nextLine();
    Log.i("Reading", "LINE: " + line);
}

Als Beispiel erzeugen wir ein Projekt, das Buttons aus einem Text-File heraus erzeugt. Das Projekt hat zunächst ein leeres LinearLayout. Fügen Sie unbedingt einen eigenen ID hinzu und stellen Sie die Orientierung auf vertical.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
</LinearLayout>

Als Ressource erzeugen wir ein Text-File wie oben beschrieben mit Namen "data.txt". Darin stehen ein paar Zeilen Text, z.B.

Alexander
Elisabeth
Johannes
Lena-Marie

In MainActivity lesen Sie die Zeilen ein und erzeugen für jede Zeile programmatisch einen Button (d.h. aus dem Code heraus und nicht im XML-File):

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    LinearLayout layout = (LinearLayout)findViewById(R.id.main_layout);

    InputStream in = getResources().openRawResource(R.raw.data);
    Scanner scanner = new Scanner(in);
    while (scanner.hasNext()) {
        String line = scanner.nextLine();
        Button b = new Button(this);
        b.setText(line);
        layout.addView(b);
    }
}

Wie Sie sehen holen Sie sich erst das Layout-Objekt, erstellen dann neue Button-Objekte und fügen Sie dem Layout hinzu.

Erzeugte Buttons

Jetzt können Sie die Textdatei erweitern:

Alexander
Elisabeth
Johannes
Lena-Marie
Christian
Christiane
Emma-Louise

Und die App nochmals starten. Sie sehen:

Erzeugte Buttons

In der Regel verwenden Sie solche Textdateien für initale Daten einer Datenbank oder Beispieldaten oder Konfigurationsinformationen.

9.2.4 Interner Speicher

Speichern

Für das Speichern von Strings holt man sich einen FileOutputStream von der Methode openFileOutput. Anschließend schreibt man die Bytes auf den Stream. Die String-Methode getBytes wandelt einen Text in die entsprechende Byte-Zahlenreihe um.

String message = "hello world";
try {
    FileOutputStream out = openFileOutput("data.txt", Context.MODE_PRIVATE);
    out.write(message.getBytes());
    out.close();
} catch (IOException e) {
    e.printStackTrace();
}

Wichtig ist es, den Stream mit close zu schließen, damit es nicht zu Datenverlust kommt. Wie in Java, kann es zu einer sogenannten Exception - speziell einer IOException - kommen. Eine Exception ist ein Fehler und das try-catch-Konstrukt ist eine Technik der Fehlerbehandlung, die wir später noch genauer kennenlernen werden.

Das Context.MODE_PRIVATE bedeutet, dass andere Apps die Datei nicht lesen dürfen. Diesen Modus sollte man auch standardmäßig verwenden und Daten über Intents mit anderen Apps teilen.

Die Datei wird in ein internes Verzeichnis gespeichert. Sie können dieses Verzeichnis mit der Methode getFilesDir auch beziehen. Wenn Sie genau wissen wollen, wo Ihre Datei gelandet ist, fügen Sie folgenden Code hinter dem out.close() ein:

File dir = getFilesDir();
Log.i("FileIO-App", "Datei gespeichert in " + dir);

Sie sehen dann in Ihrem Logcat:

I/FileIO-App: Datei gespeichert in /data/user/0/de.hsa.fileio/files

Wichtig ist, dass diese Dateien beim Deinstallieren der App gelöscht werden!

(Das kann man nur vermeiden, wenn man externen Speicher verwendet.)

Laden

Beim Laden von Texten verwendet man die Klasse Scanner. Mit einer Schleife kann man dann Texte zeilenweise einlesen.

File in = new File(getFilesDir(), "data.txt");
try {
    Scanner scanner = new Scanner(in);
    while (scanner.hasNext()) {
        String line = scanner.nextLine();
        Log.i("App", "ZEILE: " + line);
    }
  } catch (FileNotFoundException e) {
      e.printStackTrace();
  }

Alternatives Vorgehen beim Laden [optional]

Alternativ kann man die Klassen BufferedReader und FileReader verwenden. Hier benutzen wir einen StringBuffer, um die Zeilen zu einem einzigen String zusammenzubauen.

try {
    File in = new File(getFilesDir(), "data.txt");
    BufferedReader reader = new BufferedReader(new FileReader(in));
    StringBuffer sb = new StringBuffer();
    while (true) {
        String line = null;
        line = reader.readLine();
        if (line == null)
            break;
        sb.append(line);
        sb.append("\n");
    }
    String text = sb.toString();
    // Text weiterverarbeiten
} catch (IOException e) {
    e.printStackTrace();
}

Auch hier kann eine Exception auftreten.

Beispielprogramm

Als Beispielprogramm schauen wir uns folgende App an. Sie besteht aus zwei Buttons (buttonSave, buttonLoad), einem Texteingabe-Fenster (Typ EditText) und einem Textdarstellungsfenster (Typ TextView).

File IO Beispiel

Wir bekommen bei Apps mit Texteingabe manchmal ein Problem mit der Tastatur. Die Android-Tastatur wird in der Regel automatisch eingeblendet, wenn wir ein Texteingabefeld berühren. Wenn wir in unserer App auch "Speichern" klicken, verschwindet sie nicht automatisch und verdeckt so die Sicht auf die untere Hälfte der Screen. Der folgende Code gibt uns eine Funktion an die Hand, mit der wir die Tastatur programmatisch ausblenden können:

private void hideSoftKeyboard() {
    View view = getCurrentFocus();
    InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
    imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}

Wir verwenden diese Funktion, wenn wir auf "Speichern" drücken.

Hier der vollständige Code von MainActivity, wo die oben besprochenen Code-Teile zum Speichern und Laden an die jeweiligen Buttons gehängt werden.

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    final EditText input = (EditText)findViewById(R.id.inputText);
    final TextView output = (TextView)findViewById(R.id.outputText);

    ((Button)findViewById(R.id.buttonSave)).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            hideSoftKeyboard()
            try {
                FileOutputStream out = openFileOutput("data.txt", Context.MODE_PRIVATE);
                out.write(input.getText().toString().getBytes());
                out.close();
                input.setText("");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    });

    ((Button)findViewById(R.id.buttonLoad)).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            File in = new File(getFilesDir(), "data.txt");
            try {
                BufferedReader reader = new BufferedReader(new FileReader(in));
                StringBuffer sb = new StringBuffer();
                while (true) {
                    String line = reader.readLine();
                    if (line == null)
                        break;
                    sb.append(line);
                    sb.append("\n");
                }
                String text = sb.toString();
                output.setText(text);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    });
}

9.2.5 Externer Speicher [optional]

In bestimmten Fällen möchten Sie Daten in allgemein zugänglichen Ordnern Ihres Endgeräts speichern. Vielleicht wollen Sie mit anderen Apps auf die Daten zugreifen oder ganz einfach über Ihren Laptop leicht an die Daten herankommen.

Ähnlich wie Sie das von Ihrem Notebook kennen, hat auch Ihr Android-Gerät Standardordner für

Es ist sehr zu empfehlen, dass Sie sich eine App zum Browsen des Dateisystems installieren und selbst schauen, wie Ihre Verzeichnisse aussehen und heißen (einfach im Play-Store die Stichworte "Datei", "Manager" und/oder "Explorer" eingeben).

Permissions

Zunächst müssen Sie in Ihrer App anmelden, dass Sie auf "externen Speicher" zugreifen wollen. Das tun Sie im Manifest, eine XML-Datei, die Sie in Android Studio im Ordner app/manifests finden.

Dort sehen Sie z.B.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="de.hsa.filesexternalpublicstorage">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        ...>
        ...
    </application>
</manifest>

Fügen Sie die Zeile mit uses-permission ein:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="de.hsa.filesexternalpublicstorage">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application ... >
        ...
    </application>

</manifest>

Verzeichnis finden

Wir möchten auf ein Verzeichnis zugreifen, das bereits existiert und evtl. dort ein Unterverzeichnis anlegen. Das Verzeichnis Documents finden wir über die statische Methode Environment.getExternalStoragePublicDirectory. Diese Methode bekommt einen Parameter, der besagt, welches Standardverzeichnis (Dokumente, Bilder, ...) wir haben möchten. Für Documents:

File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);

Wir erhalten das Verzeichnis als Objekt vom Typ File.

Erlaubnis

Auch wenn wir bereits die prinzipielle Erlaubnis haben, in ein externes Verzeichnis zu schreiben (siehe Manifest oben), müssen wir seit API-Level 23 auch die Erlaubnis des Benutzers einholen. Dies erledigt die Methode requestPermissions.

Das muss natürlich für diese App nur einmalig gemacht werden, daher fragen wir mit der statischen Methode ActivityCompat.checkSelfPermission, ob die Erlaubnis bereits vorliegt.

Im folgenden Code wird zunächst abgefragt, ob das Endgerät API-Level 23 oder höher hat. Dann schauen wir, ob wir die Erlaubnis schon haben. Wenn nicht, erfragen wir sie.

if (android.os.Build.VERSION.SDK_INT >= 23) {

    int permission = ActivityCompat.
            checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);

    if (permission != PackageManager.PERMISSION_GRANTED) {
        this.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 200);
    }
}

Speichern

Der Code zum Speichern sieht jetzt minimal anders aus als beim internen Speicher. Zunächst haben wir das Ziel-Verzeichnis (s.o.):

File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);

Jetzt stellen wir ein File-Objekt für die Ziel-Datei her. Auch dazu nutzen wir File:

File file = new File(dir, "mytext.txt");

Jetzt erzeugen wir ein Objekt vom Typ FileOutputStream, das wir letztlich fürs Schreiben benutzen:

FileOutputStream out = new FileOutputStream(file);

Das Schreiben sieht so aus. Wir gehen davon aus, dass wir eine Text-Komponente mit Namen input haben:

out.write(input.getText().toString().getBytes());

Und schließlich müssen wir den Stream schließen, um keine Daten zu verlieren:

out.close();

Der gesamte Code muss noch von einem try-catch umgeben werden, um gegebenenfalls einen Fehler abzufangen.

Laden

Das Laden funktioniert sehr ähnlich wie beim internen Speicher, nur dass wir das Verzeichnis wie beim Speichern mit getExternalStoragePublicDirectory beziehen:

File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
File in = new File(dir, "mytext.txt");
try {
    BufferedReader reader = new BufferedReader(new FileReader(in));
    StringBuffer sb = new StringBuffer();
    while (true) {
        String line = reader.readLine();
        if (line == null)
            break;
        sb.append(line);
        sb.append("\n");
    }
    String text = sb.toString();
    output.setText(text);
} catch (IOException e) {
    e.printStackTrace();
}

Beispielprogramm

Schauen wir uns wieder ein Beispielprogramm an.

File IO Beispiel

Auch hier besteht die App aus zwei Buttons (buttonSave, buttonLoad), einem Texteingabe-Fenster (Typ EditText) und einem Textdarstellungsfenster (Typ TextView).

Hier der Code von MainActivity:

protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  final EditText input = (EditText)findViewById(R.id.inputText);
  final TextView output = (TextView)findViewById(R.id.outputText);

  ((Button)findViewById(R.id.buttonSave)).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
          try {
              File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);

              if (android.os.Build.VERSION.SDK_INT >= 23) {
                  int permission = ActivityCompat.
                          checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE);

                  if (permission != PackageManager.PERMISSION_GRANTED) {
                      MainActivity.this.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 200);
                  }
              }

              File file = new File(dir, "mytext.txt");
              FileOutputStream out = new FileOutputStream(file);
              out.write(input.getText().toString().getBytes());
              out.close();
              input.setText("");
              Log.i("FileIO-App", "Datei gespeichert in " + file);
          } catch (IOException e) {
              e.printStackTrace();
          }
      }
  });

  ((Button)findViewById(R.id.buttonLoad)).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
          File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
          File in = new File(dir, "mytext.txt");
          try {
              BufferedReader reader = new BufferedReader(new FileReader(in));
              StringBuffer sb = new StringBuffer();
              while (true) {
                  String line = reader.readLine();
                  if (line == null)
                      break;
                  sb.append(line);
                  sb.append("\n");
              }
              String text = sb.toString();
              output.setText(text);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    });
}

Zu beachten ist hier, dass Sie an zwei Stellen "MainActivity.this" stehen haben. Dies liegt daran, dass sie aus einer inneren (anonymen) Klasse heraus auf "this" zugreifen wollen. Daher müssen Sie ein "MainActivity" voranstellen (da das "this" sich sonst auf die innere Klasse bezieht).

Eigenes Unterverzeichnis

Wenn Sie im Standardverzeichnis Documents ein eigenes Unterverzeichnis (z.B. "MyDocs") anlegen wollen, können Sie auch hier die Klasse File nutzen.

File dir = new File(Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_DOCUMENTS), "MyDocs");

Ersetzen Sie die zwei Stellen, an denen Sie ein Verzeichnis anfordern (beim Speichern und beim Laden) durch diese Variante.

9.3 Zusammenfassung

Wenn Sie eine App schließen, können Sie Daten über zwei Mechanismen sichern: SharedPreferences oder Dateien.

Bei den SharedPreferences handelt es sich um Tabellen. In Tabellen werden Informationen immer in Form von Schlüssel-Wert-Paaren (engl. key-value pair) gespeichert. Sie können eine eigene Tabelle erzeugen, die auch beim Schließen der App erhalten bleibt. Dieser Tabelle müssen Sie einen Namen (String) geben.

Zum Erzeugen und Abrufen der Tabellen nutzen Sie die Methode getSharedPreferences. Zum Schreiben von Schlüssel-Wert-Paaren beziehen Sie zunächst ein Editor-Objekt mit edit und verwenden dann put-Methoden - z.B. z.B. putString oder putInt - für das Hinzufügen von Informationen. Das Schreiben wird mit apply abgeschlossen. Zum Lesen haben Sie ebenfalls typspezifische get-Methoden, z.B. getString oder getInt.

Bei Dateien unterscheiden wir zwischen einer Datei als Ressource, einer Datei im internen Speicher (der App) und einer Datei im externen Speicher (geräteweiter Zugriff). Bei allen drei Methoden benötigen Sie die Klasse Scanner zu einlesen, i.d.R. mit Hilfe einer Schleife. Für das Speichern verwendet man die Klasse FileOutputStream. In allen Fällen ist es wichtig, die entsprechenden Lese-/Schreibobjekte mit close zu schließen.

Beim Speichern in den externen Speicher, müssen entsprechende Permissions (WRITE_EXTERNAL_STORAGE) vorliegen. Zudem wird der Benutzer von Android gefragt, ob der Zugriff erlaub werden soll.

9.4 Übungen

(A) ZählerApp v2

Implementieren Sie eine zweite Version der ZählerApp aus dem Modul "Daten I", diesmal mit Hilfe von Preferences.

Zähler-App

(B) FortuneCookieApp

Schreiben Sie eine App, die auf Knopfdruck einen neuen Glückskeks-Spruch anzeigt. Verwenden Sie eine Textdatei in den Ressourcen, wo in jeder Zeile ein Spruch steht. Sie finden so etwas, wenn Sie nach "fortune cookie quotes" googlen.

FortuneCookieApp Portrait

FortuneCookieApp Landscape

(C) Speichern in den internen Speicher

Bauen Sie die in diesem Modul skizzierte App, die in den internen Speicher speichert, nach.

(D) Speichern in den externen Speicher

Bauen Sie die in diesem Modul skizzierte App, die in den externen Speicher speichert, nach. Installieren Sie eine Dateimanager-App auf Ihrem Endgerät und prüfen Sie, ob die Datei dort ist, wo Sie sie vermuten.