Scoped Storage in Android
Scoped Storage begrenzt Dateizugriffe und schützt Nutzerdaten. Du lernst, wann App-Speicher, MediaStore oder Picker passen.
Scoped Storage ist ein Kernbaustein moderner Android-Speicherung: Deine App bekommt nicht mehr automatisch freien Zugriff auf große Teile des gemeinsamen Gerätespeichers. Stattdessen entscheidest du bewusst, ob eine Datei privat zur App gehört, ob sie als nutzersichtbares Medium im System auftauchen soll oder ob der Nutzer eine Datei über einen Picker gezielt auswählt.
Was ist das?
Scoped Storage beschreibt das Speicher-Modell, mit dem Android den Zugriff einer App auf Dateien begrenzt. Vor Android 10 war es üblich, mit breiten Speicherberechtigungen auf viele Dateien im externen Speicher zuzugreifen. Das war bequem, aber aus Sicht von Datenschutz und Sicherheit problematisch: Eine Notizen-App musste nicht wissen, welche Urlaubsfotos, Downloads oder Dokumente auf dem Gerät liegen.
Das mentale Modell ist: Deine App arbeitet in ihrem eigenen Bereich und bittet nur dann um Zugriff auf fremde oder nutzersichtbare Dateien, wenn es fachlich nötig ist. Dieser Zugriff soll möglichst eng sein. Wenn du ein Profilbild speicherst, das nur deine App braucht, gehört es in app-spezifischen Speicher. Wenn du ein Foto erzeugst, das in der Galerie sichtbar sein soll, ist MediaStore der passende Weg. Wenn der Nutzer ein PDF aus einem beliebigen Ordner öffnen möchte, lässt du ihn die Datei über einen System-Picker auswählen.
Android 10 ist hier ein wichtiger Wendepunkt, weil Scoped Storage dort eingeführt wurde. Spätere Android-Versionen haben das Modell weiter geschärft. Für dich als Lernenden ist aber nicht die Historie entscheidend, sondern die Arbeitsweise: Du planst Dateizugriff nicht mehr als freien Pfadzugriff, sondern als bewusstes Daten-Design.
Im Android-Alltag betrifft dich das an vielen Stellen. Du schreibst vielleicht Cache-Dateien, exportierst Berichte, lädst Bilder offline vor, speicherst Anhänge oder zeigst Medien aus der Galerie. Jede dieser Aufgaben braucht eine andere Storage-Entscheidung. Das Thema hängt damit direkt an Architektur, Daten-Schicht, Offline-First-Denken und Release-Qualität. Eine App, die lokal funktioniert, aber auf neueren Geräten keine Dateien mehr findet oder unnötige Berechtigungen anfragt, wirkt schnell unfertig und kann im Review auffallen.
Wie funktioniert es?
Scoped Storage trennt grob zwischen privaten App-Daten, gemeinsam sichtbaren Medien und vom Nutzer ausgewählten Dokumenten. Diese Trennung ist wichtiger als einzelne API-Namen.
Private App-Daten liegen in Verzeichnissen, die deiner App gehören. Dazu zählen interne Dateien, Caches und app-spezifische externe Verzeichnisse. Dort kannst du ohne breite Speicherberechtigung lesen und schreiben. Solche Dateien sind sinnvoll für Session-Daten, heruntergeladene JSON-Antworten, temporäre Bilder, Datenbank-Dateien oder vorberechnete Offline-Inhalte. Wenn die App deinstalliert wird, verschwinden diese Daten in der Regel mit ihr. Das ist oft genau richtig, weil sie nicht als persönliche Dokumente des Nutzers gedacht sind.
Gemeinsam sichtbare Medien sind zum Beispiel Bilder, Videos oder Audiodateien, die der Nutzer auch außerhalb deiner App sehen soll. Dafür nutzt du MediaStore. Statt beliebige Pfade zu bauen, fügst du Einträge über einen ContentResolver ein und schreibst in den zugehörigen Uri. Das System verwaltet Speicherort, Sichtbarkeit und Indexierung. Deine App arbeitet also mit Uri-Werten und Metadaten statt mit absoluten Dateipfaden.
Dokumente und fremde Dateien erreichst du über System-Picker, etwa über das Storage Access Framework oder moderne Picker-APIs für Medien. Der Nutzer entscheidet sichtbar, welche Datei oder welcher Ordner freigegeben wird. Deine App erhält danach einen Uri und arbeitet damit. Dieses Modell passt gut zu Datenschutz, weil die Freigabe aus einer konkreten Nutzeraktion entsteht.
Für Kotlin- und Jetpack-Apps heißt das: Dateizugriff gehört selten direkt in eine Composable. Jetpack Compose beschreibt UI-Zustand und Nutzerinteraktion, aber Speicherzugriff ist Aufgabe einer Daten-Schicht oder eines Repository. Das Repository kann mit ContentResolver, Datenbank, Cache und Netzwerk sprechen. Ein ViewModel ruft diese Schicht auf und stellt den UI-Zustand bereit. So bleibt deine Oberfläche testbarer, und du kannst Fehlerfälle sauber abbilden: kein Zugriff, Datei nicht gefunden, Speicher voll, Schreibvorgang abgebrochen oder Nutzer hat den Picker geschlossen.
Bei größeren Dateien und I/O-Operationen solltest du außerdem Coroutines richtig einsetzen. Lesen und Schreiben ist blockierende Arbeit und gehört nicht auf den Main Thread. In einer sauberen Architektur nutzt du dafür einen geeigneten Dispatcher, typischerweise Dispatchers.IO, und kapselst die Arbeit in suspend-Funktionen. Das verbessert nicht nur die Bedienbarkeit, sondern verhindert auch schwer auffindbare UI-Hänger.
Wichtig ist auch: Scoped Storage ist kein einzelnes Permission-Thema. Berechtigungen sind nur ein Teil davon. Der größere Gedanke ist Datenminimierung. Du fragst nicht nach maximalem Zugriff, weil es einfacher zu programmieren wäre. Du wählst die engste API, die den konkreten Nutzerfall erfüllt.
In der Praxis
Eine gute Entscheidungsregel lautet: Frage zuerst, wem die Datei gehört und wer sie sehen soll. Wenn die Datei nur deine App braucht, nutze app-spezifischen Speicher. Wenn der Nutzer sie in Galerie, Musik-App oder ähnlichen Systemoberflächen sehen soll, nutze MediaStore. Wenn der Nutzer eine vorhandene Datei auswählt, arbeite mit einem vom System gelieferten Uri. Wenn du diese Frage nicht beantworten kannst, ist das ein Warnsignal im Design.
Ein typisches Beispiel ist ein erzeugtes Textprotokoll, das nur deine App intern benötigt. Dafür brauchst du keinen Zugriff auf Downloads und keine breite Speicherberechtigung. Du kannst die Datei intern speichern und den Zugriff über dein Repository kapseln:
class LogFileRepository(
private val context: Context,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun saveLocalLog(fileName: String, content: String): Result<File> =
withContext(ioDispatcher) {
runCatching {
val safeName = fileName.replace(Regex("[^a-zA-Z0-9._-]"), "_")
val file = File(context.filesDir, safeName)
file.writeText(content)
file
}
}
suspend fun readLocalLog(fileName: String): Result<String> =
withContext(ioDispatcher) {
runCatching {
val safeName = fileName.replace(Regex("[^a-zA-Z0-9._-]"), "_")
File(context.filesDir, safeName).readText()
}
}
}
Der Code zeigt mehrere praktische Punkte. Erstens liegt die Datei im privaten Bereich der App. Zweitens läuft der I/O-Zugriff über Dispatchers.IO. Drittens wird der Dateiname begrenzt, statt ungeprüft aus Nutzereingaben einen Pfad zu bauen. Viertens gibt das Repository Result zurück, damit die aufrufende Schicht Fehler gezielt behandeln kann. In einer Compose-App würdest du diese Methoden aus dem ViewModel aufrufen und das Ergebnis als UI-State anzeigen.
Wenn dasselbe Protokoll als Datei für den Nutzer exportiert werden soll, ist der private Speicher nicht mehr zwingend passend. Dann kannst du dem Nutzer über einen Dokument-Picker erlauben, einen Speicherort zu wählen, oder du nutzt eine passende System-API für einen nutzersichtbaren Export. Der Unterschied ist nicht nur technisch, sondern fachlich: Internes Log, öffentlicher Export und fremdes Dokument sind unterschiedliche Fälle.
Eine häufige Stolperfalle ist der Griff zu alten Pfadmustern. Code wie File(“/sdcard/Download/report.txt”) ist brüchig. Er kann auf bestimmten Geräten, Android-Versionen oder Profilkonfigurationen scheitern. Noch problematischer ist es, aus Bequemlichkeit breite Berechtigungen anzufragen, obwohl deine App nur eine einzelne Datei braucht. Das erschwert Vertrauen, kann Nutzer irritieren und passt nicht zu moderner Android-Entwicklung.
Eine zweite Stolperfalle ist die Vermischung von UI und Storage. Wenn eine Composable direkt Dateien schreibt, wird sie schwer testbar und reagiert schlecht auf Lebenszyklusänderungen. Besser ist ein klarer Fluss: UI löst eine Aktion aus, ViewModel koordiniert, Repository schreibt oder liest, UI zeigt Zustand und Fehler an. So passt Storage in die Architektur deiner App, statt als verstreuter Seiteneffekt im Code aufzutauchen.
Für Offline-First-Apps ist Scoped Storage besonders relevant. Du kannst Daten lokal vorhalten, aber du solltest unterscheiden, ob es sich um strukturierte App-Daten, Cache, Medien oder nutzersichtbare Exporte handelt. Strukturierte Daten gehören oft in eine Datenbank. Große Binärdaten können in app-spezifischem Speicher liegen und über deine Daten-Schicht referenziert werden. Dateien, die der Nutzer unabhängig von der App behalten soll, brauchen einen nutzersichtbaren Speicherweg. Diese Entscheidung beeinflusst Synchronisation, Löschverhalten und Fehlerbehandlung.
Auch beim Testen kannst du viel prüfen. Unit-Tests können deine Dateinamenslogik und Repository-Fehlerfälle abdecken. Instrumentation-Tests können auf einem Emulator prüfen, ob Schreiben und Lesen in app-spezifischem Speicher funktionieren. Im Debugger kannst du kontrollieren, welche Uri-Werte zurückkommen und ob dein Code wirklich nicht auf absolute Pfade angewiesen ist. Im Code-Review solltest du gezielt nach drei Dingen suchen: unnötige Speicherberechtigungen, harte Pfade und blockierende I/O-Arbeit auf dem Main Thread.
Beachte außerdem die Nutzerperspektive. Wenn deine App eine Datei speichert, sollte klar sein, ob diese Datei nur in der App existiert oder später auch außerhalb sichtbar ist. Unklare Speicherung führt zu Support-Fragen: Der Nutzer erwartet eine Datei im Download-Ordner, du hast sie aber intern gespeichert. Oder umgekehrt: Du erzeugst öffentliche Dateien, obwohl sie nur temporär gebraucht werden. Scoped Storage zwingt dich damit zu präziseren Produktentscheidungen.
Fazit
Scoped Storage ist kein lästiges Detail, sondern ein praktisches Modell für sauberen, datensparsamen Dateizugriff auf Android. Du solltest bei jeder Datei klären, ob sie privat zur App gehört, als Medium sichtbar sein soll oder aus einer bewussten Nutzerauswahl stammt. Prüfe dieses Wissen aktiv: Baue ein kleines Repository für interne Dateien, ergänze einen Export-Fall über einen System-Picker, beobachte die Uri-Werte im Debugger und lass im Code-Review gezielt nach alten Pfadzugriffen, unnötigen Permissions und Main-Thread-I/O suchen.