skip to content

On-Device Persistence

Where a Flutter app keeps data between launches: key-value preferences, SQLite via sqflite or Drift, secure storage for secrets, and files in app directories. Interviewers probe choosing among them.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

24

In Flutter's path_provider, how do the documents, support, temporary and cache directories differ, and what does each map to on iOS and Android?

level: juniorimportance: must knowfreq 60%

answer

  1. who created the file, who may delete it
  2. documents: user data that cannot be recreated
  3. support: app files hidden from the user
  4. temporary and cache: purgeable
  5. Android getCacheDir, iOS Library/Caches

basics

~20 s

Documents holds user-generated or unrecreatable data, support holds app-private files, and temporary and cache hold re-creatable data the OS may clear. On iOS they map to Documents, Application Support and Caches; on Android to app_flutter, files and the cache dir.

solid answer

~40 s

`path_provider` returns `Directory` objects for platform locations; you then use `dart:io` to read and write inside them. `getApplicationDocumentsDirectory()` is for data the user created or that cannot be recreated: `NSDocumentDirectory` on iOS, an `app_flutter` directory from `Context.getDir` on Android. `getApplicationSupportDirectory()` is for app files you do not expose to the user, such as a database: Application Support on iOS, `Context.getFilesDir()` on Android, created if missing. `getTemporaryDirectory()` and `getApplicationCacheDirectory()` are for data you can fetch or rebuild; on Android and iOS they resolve to the same directory (`getCacheDir()` and `NSCachesDirectory`), which the system may clear. All of them are app-scoped on mobile and are removed when the app is uninstalled.

code

dart · 16 lines
dart
import 'dart:io';

import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

Future<File> offlineNoteFile(String courseId, String noteId) async {
  final docs = await getApplicationDocumentsDirectory();
  final dir = Directory(p.join(docs.path, 'notes', courseId));
  await dir.create(recursive: true);
  return File(p.join(dir.path, '$noteId.pdf'));
}

Future<File> thumbnailFile(String noteId) async {
  final cache = await getApplicationCacheDirectory();
  return File(p.join(cache.path, 'thumbs', '$noteId.png'));
}

go deeper

for a junior

Recall the four functions and one-line purposes: documents for user data, support for app files, temporary and cache for purgeable data.

for a middle

Explain the iOS and Android mappings, including that temporary and cache coincide on mobile, and why you store relative paths.

for a senior

Map each kind of file in a real feature to a directory, and plan for purging, partial downloads and backup size.

for a principal

Set a storage policy across the app: which data is user-owned, which is re-creatable, and how that shapes quotas and support costs.

## What path_provider is for A Flutter app that writes files, such as a study app that saves PDF lecture notes for offline reading, first needs to know **where** it is allowed to write. Each operating system has its own sandbox layout and conventions. The **`path_provider`** plugin (2.1.x, maintained in the flutter/packages repository) hides those differences behind a few async functions that return `dart:io` `Directory` objects. Reading and writing files inside them is then ordinary `dart:io` work. The functions differ less in *where* they point than in the **contract** each directory carries: who owns the data, whether the system may delete it, and whether it is backed up. ## The four everyday directories | Function | Intended content | iOS | Android | |---|---|---|---| | `getApplicationDocumentsDirectory()` | user-generated data, or data that cannot be recreated | `NSDocumentDirectory` | `Context.getDir("flutter")`, the `app_flutter` folder | | `getApplicationSupportDirectory()` | app files not meant for the user, such as a database or config | `NSApplicationSupportDirectory`, created if missing | `Context.getFilesDir()` | | `getTemporaryDirectory()` | scratch files and caches that may vanish | `NSCachesDirectory` | `Context.getCacheDir()` | | `getApplicationCacheDirectory()` | app-specific caches | `NSCachesDirectory`, created if missing | `Context.getCacheDir()` | Two details surprise people: - On **Android and iOS, `getTemporaryDirectory()` and `getApplicationCacheDirectory()` return the same directory**. The separate cache function exists mainly because the desktop platforms have distinct cache locations. - On iOS, the "temporary" directory is `Library/Caches`, not the `tmp` folder. The plugin's documentation says files there may be cleared at any time, and that the call does not create a fresh temporary folder: you create and clean up your own files inside it. ## Choosing for the lecture-notes app 1. A PDF the user explicitly downloads to read offline is data the user asked to keep. Losing it silently would be a bug, so it belongs in **documents** or **support**, not in a cache. 2. A thumbnail rendered from the first page can be rebuilt at any time, so it belongs in the **cache** directory. 3. The app's own database of which notes are downloaded is app data, not a user document, so it belongs in **support**. 4. A half-finished download goes to **temporary** first and is moved into place only when complete, so a crash never leaves a truncated PDF where the reader expects a good one. ## Other functions in the same package - `getLibraryDirectory()` is iOS and macOS only; calling it on other platforms throws. - `getExternalStorageDirectory()`, `getExternalStorageDirectories()` and `getExternalCacheDirectories()` are Android only and return app-specific folders on shared or removable storage. - `getDownloadsDirectory()` returns a downloads location whose meaning differs by platform and may be `null`. When a directory that should exist cannot be obtained, the functions throw `MissingPlatformDirectoryException`. None of them has a web implementation. ## How the plugin reaches the platform `path_provider` is a **federated plugin**: the app depends on `path_provider`, which endorses one implementation package per platform (`path_provider_android`, `path_provider_foundation` for iOS and macOS, `path_provider_linux`, `path_provider_windows`). Recent releases of the Android and Apple implementations call platform APIs directly through JNI and FFI rather than a method channel; the Dart API you call is unchanged. That design matters in tests. A widget or unit test has no real platform, so the plugin's README recommends replacing `PathProviderPlatform.instance` with a fake rather than mocking a method channel. The package's own tests do this with a class that extends `Fake`, mixes in `MockPlatformInterfaceMixin` and implements `PathProviderPlatform`, returning paths under a temporary test folder. Code that receives its directories through a small injected service is easier still to test. ## Practical rules - Call the function each time you need a path, or cache the `Directory` for the session, but never persist an **absolute** path; the directory's location can change, for example across iOS reinstalls or restores. - Store paths **relative** to the directory you chose, and join them at runtime with `package:path`. - Keep large, re-downloadable files out of backed-up directories when you can; the next question in an interview is usually about backups.

  • With path_provider on Android, why would getApplicationDocumentsDirectory() not show up in the device's Files app?
    On Android it returns an app-private folder created with `Context.getDir("flutter")`, inside the app's internal storage. Other apps and the user's file manager cannot see it. It is private storage despite the name "documents".
  • Does path_provider's getTemporaryDirectory() give each call a fresh, empty folder?
    No. It returns the same app-scoped cache location every time, on iOS `Library/Caches` and on Android `getCacheDir()`. Your code creates, names and deletes its own files or subfolders inside it, and must expect the system to remove them.

saying these in an interview costs you the question

  • getTemporaryDirectory() returns a new empty folder on each call.
  • Anything in the cache directory stays until the app deletes it.
  • getApplicationDocumentsDirectory() on Android is the shared Documents folder.
  • Persisting the absolute path of a saved file is safe across updates.
  • Temporary and cache directories are different folders on Android and iOS.
open as a page

In Flutter, what is shared_preferences meant to store, which value types does it accept, and what should never go into it?

level: juniorimportance: must knowfreq 62%

basics

~20 s

shared_preferences holds small, non-critical settings as int, double, bool, String or List<String>, backed by NSUserDefaults, Android DataStore or SharedPreferences, and localStorage on web. Never store secrets, large data or records you cannot afford to lose.

open as a page

In a Flutter app, why does a refresh token belong in flutter_secure_storage rather than shared_preferences, and where does each platform keep it?

level: juniorimportance: must knowfreq 65%

basics

~20 s

shared_preferences writes plain values to an app file anyone with file access can read, while flutter_secure_storage encrypts them with keys held by the OS: the Keychain on iOS and macOS, Keystore-wrapped AES keys on Android.

open as a page

In a Flutter app using Drift, how does a query's watch() stream know to re-emit after a write, and where does that mechanism break down?

level: middleimportance: must knowfreq 55%

basics

~20 s

Drift records which tables each watched query reads; every insert, update or delete made through drift's APIs marks those tables changed, and affected queries re-run and emit. Writes that bypass drift's notifications, such as customStatement or another connection, trigger nothing.

open as a page

With sqflite, how do openDatabase's version, onCreate and onUpgrade work together when an app adds a column to an existing table in version 2?

level: middleimportance: must knowfreq 55%

basics

~20 s

openDatabase compares the requested version with the file's stored version. A new file runs onCreate, which must build the full current schema; an older file runs onUpgrade(db, oldVersion, newVersion) once, where you apply each step, such as ALTER TABLE ADD COLUMN.

open as a page

Your Flutter app's Drift schema gains a new column in the next release; how do you migrate existing users' databases safely and prove it works?

level: seniorimportance: must knowfreq 50%

basics

~10 s

Bump schemaVersion, handle the step in MigrationStrategy.onUpgrade (ideally the generated stepByStep with m.addColumn), and let make-migrations export schema snapshots and generate tests that migrate a v1 database with SchemaVerifier and validate the result.

open as a page

After a user reinstalls or restores your Flutter app, what can happen to tokens saved with flutter_secure_storage on iOS and Android, and how do you handle it?

level: seniorimportance: must knowfreq 45%

basics

~20 s

On iOS, Keychain items can survive uninstall, so a reinstalled app may find an old token; clear secure storage on first run. On Android, a restored backup brings the encrypted file but not the Keystore key, so values cannot be decrypted.

open as a page

In Drift for Flutter, what do you write by hand for a table, a database and a DAO, and what does the generator produce?

level: juniorimportance: should knowfreq 45%

basics

~20 s

You write Table subclasses with column getters, a database class annotated @DriftDatabase that extends the generated _$AppDatabase and declares schemaVersion, and optional @DriftAccessor DAOs. Drift's generator writes typed row classes, companions, table getters and DAO mixins.

open as a page

In sqflite, how do the insert and query helpers relate to rawInsert and rawQuery, and why must values go through arguments rather than string interpolation?

level: juniorimportance: should knowfreq 45%

basics

~20 s

insert, query, update and delete build the SQL from a table name and maps; rawInsert and rawQuery take your SQL. Either way values go in as ? placeholders with whereArgs or arguments, which binds them safely and avoids quoting bugs and SQL injection.

open as a page

In Drift's Dart query API, how do you join two tables and read typed rows from the result, including rows with no match?

level: middleimportance: should knowfreq 30%

basics

~10 s

Start with select(habits).join([leftOuterJoin(checkIns, checkIns.habit.equalsExp(habits.id))]); get() or watch() then yields TypedResult rows, read with readTable(habits) and readTableOrNull(checkIns) for the side that may be missing.

open as a page

A Flutter app saves PDF lecture notes for offline reading; which path_provider directory should hold them, and what may purge or back them up?

level: middleimportance: should knowfreq 40%

basics

~20 s

Notes the user chose to keep belong in documents or support, because the OS may delete cache contents under storage pressure. Those persistent directories are backed up by default on iOS and by Android Auto Backup, so large re-downloadable PDFs inflate backups.

open as a page

In shared_preferences 2.5, how do SharedPreferencesAsync and SharedPreferencesWithCache differ, and why are both preferred over the legacy getInstance API?

level: middleimportance: should knowfreq 38%

basics

~20 s

SharedPreferencesAsync has no cache, so every read is an awaited platform call that always sees the latest value. SharedPreferencesWithCache loads allow-listed keys once in create() and then reads synchronously. The legacy getInstance singleton is slated for deprecation.

open as a page

In Flutter, how do you read a saved language and an onboarding flag from shared_preferences before the first frame, avoiding a flash of the wrong screen?

level: middleimportance: should knowfreq 40%

basics

~10 s

In main, call WidgetsFlutterBinding.ensureInitialized(), await SharedPreferencesWithCache.create with an allowList of the two keys, then pass the instance into runApp so the first build reads locale and onboarding state synchronously.

open as a page

With flutter_secure_storage on iOS, how does IOSOptions accessibility decide when a stored token can be read, and why might a background refresh fail?

level: middleimportance: should knowfreq 30%

basics

~20 s

IOSOptions.accessibility maps to the Keychain's kSecAttrAccessible. The default, unlocked, makes items readable only while the device is unlocked, so a background refresh on a locked phone fails; first_unlock allows reads after the first unlock since boot.

open as a page

In sqflite, what is the difference between db.transaction and db.batch, and when would you choose each?

level: middleimportance: should knowfreq 42%

basics

~20 s

A transaction runs awaited statements all-or-nothing and lets you read results mid-way; you must use its txn object. A batch queues statements and sends them in one commit, atomically by default, without intermediate reads, which makes bulk writes fast.

open as a page

In a Flutter app using Drift, how do you keep SQLite work off the UI isolate, and what changes when a background isolate needs the same database?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Open the database with NativeDatabase.createInBackground or drift_flutter's driftDatabase, which run SQLite on a drift-managed isolate. A background isolate must connect to that same instance (shareAcrossIsolates, DriftIsolate, computeWithDatabase) rather than open its own copy.

open as a page

Your Flutter app's offline PDF folder keeps growing; how do you implement a safe clear-cache action and track files so nothing breaks afterwards?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Delete only the contents of the cache directory, never saved notes; keep an index of downloads with paths relative to getApplicationSupportDirectory(); resolve absolute paths at runtime; skip in-flight downloads; and let the index drive re-downloads when files go missing.

open as a page

When a Flutter app that saves files with path_provider also targets web and desktop, what breaks or behaves differently, and how do you adapt?

level: seniorimportance: should knowfreq 25%

basics

~20 s

path_provider has no web implementation and dart:io files do not exist in browsers, so web needs browser storage or a download prompt. On Windows and Linux, documents is the user's shared Documents folder and temporary is system-wide, so app data belongs in support.

open as a page

How do you make a Flutter app require biometrics before flutter_secure_storage releases a secret on Android and iOS, and what can go wrong?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Use AndroidOptions.biometric(enforceBiometrics: true) on Android and IOSOptions(accessControlFlags: [...]) such as biometryCurrentSet or userPresence on iOS, so the OS demands authentication before the key or item is used, not just before a screen opens.

open as a page

A Flutter app using sqflite hangs on writes or throws 'database is locked' on Android; what usually causes it, and how should database access be structured?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Usually the same file is opened several times with singleInstance: false, db is used instead of txn inside a transaction, or another isolate holds or closes the connection. Open one Database for the app lifetime through a cached Future and keep transactions short.

open as a page

What does path_provider's getDownloadsDirectory() return on Android, iOS and desktop, and why is it not Android's public Downloads folder?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

getDownloadsDirectory() returns Directory?; on Android it is the app-specific Downloads folder under external app storage, not the shared one; on desktop it is typically the user's Downloads folder; null means no such directory is available.

open as a page

In flutter_secure_storage 10, what replaced Android's encryptedSharedPreferences option, and how do the new cipher defaults and migration flags behave?

level: middleimportance: nice to knowfreq 20%

basics

~10 s

Version 10 deprecated encryptedSharedPreferences because Jetpack Security is deprecated, and switched to custom ciphers: an RSA-OAEP key cipher wrapping an AES-GCM storage cipher. Data migrates automatically because migrateOnAlgorithmChange defaults to true.

open as a page

What does sqflite_common_ffi add to a Flutter project using sqflite, and how do you use it to unit-test database code?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

sqflite_common_ffi is a Dart FFI implementation of the sqflite API over a native SQLite library. It runs on Windows, Linux and macOS and in plain unit tests, where the sqflite plugin has no platform side; call sqfliteFfiInit() and set databaseFactory = databaseFactoryFfi.

open as a page

When a Flutter app switches from SharedPreferences.getInstance() to SharedPreferencesAsync, why can saved values seem to disappear, and how do you migrate them safely?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

The legacy API stores keys with a hidden flutter. prefix, and on Android the new APIs default to DataStore instead of the old SharedPreferences file, so the new API reads different keys. Run migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary once at start-up before any new read.

open as a page