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?
answer
- clear contents, not the directory contract
- relative paths resolved at runtime
- index is the source of truth
- skip files still being written
- measure size by listing entries
basics
~20 sDelete 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.
solid answer
~50 sI separate what a clear-cache action may touch from what it must not. The action lists the entries under `getApplicationCacheDirectory()` and deletes them, which also covers `getTemporaryDirectory()` on Android and iOS because both resolve to the same folder, while skipping any file a running download still writes. Saved PDFs live under `getApplicationSupportDirectory()` and are removed only through an explicit "remove downloads" action that also updates the index. The index, a small database, stores each note's path relative to the support directory, its size and its state; absolute paths are rebuilt at runtime because the container path can change, for example after an iOS reinstall or restore. Sizes shown in settings come from summing file lengths, and on startup the app reconciles the index with the disk so a purged or missing file becomes "not downloaded" instead of a crash.
code
dart · 30 linesimport 'dart:io';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
Future<int> clearCache({Set<String> inFlight = const {}}) async {
final cache = await getApplicationCacheDirectory();
var freed = 0;
await for (final entity in cache.list()) {
if (inFlight.contains(p.basename(entity.path))) continue;
freed += await _sizeOf(entity);
await entity.delete(recursive: true);
}
return freed;
}
Future<int> _sizeOf(FileSystemEntity entity) async {
if (entity is File) return entity.length();
if (entity is! Directory) return 0;
var total = 0;
await for (final child in entity.list(recursive: true)) {
if (child is File) total += await child.length();
}
return total;
}
Future<File> resolveNote(String relativePath) async {
final support = await getApplicationSupportDirectory();
return File(p.join(support.path, relativePath));
}go deeper
Recall that clearing a cache must never remove files the user chose to keep.
Explain why temporary and cache coincide on mobile and how to list and delete a directory's contents safely.
Design the index with relative paths and states, reconciliation on startup, and crash-safe ordering between file and row deletion.
Decide storage limits, eviction and user controls for offline content, balancing offline promises against device space.
## Why this is a senior question Writing a file with **`path_provider`** and `dart:io` is easy. Keeping hundreds of downloaded PDF lecture notes consistent over months of app updates, OS purges, user actions and crashes is not. Interviewers use a "clear cache" feature to see whether a candidate thinks about **state that lives on disk** as carefully as state in memory. ## Three kinds of files, three policies | Files | Directory | Who may delete them | |---|---|---| | Saved PDFs | `getApplicationSupportDirectory()/notes` | only the user, via "remove downloads" | | Previews, thumbnails | `getApplicationCacheDirectory()/thumbs` | the app's clear-cache action, or the OS | | Partial downloads | `getTemporaryDirectory()` | the download code on success or failure, or the OS | On Android and iOS, the cache and temporary functions resolve to the **same** directory, so a clear-cache action that empties the cache also sees partial downloads. That is why the action must skip files that are still being written. ## Implementing clear cache 1. Resolve the cache directory with `getApplicationCacheDirectory()`. 2. List its entries and delete each one recursively, instead of deleting the directory itself. The function recreates the directory on iOS if missing, but other code may be holding it open. 3. Skip entries that belong to active downloads, for example by keeping in-flight names in a set owned by the download service, or by using a dedicated subfolder for partial files that the action never touches. 4. Recompute the size shown in settings afterwards. Measuring size means listing files recursively and summing their lengths; there is no single call that reports a directory's size. Do it off the hot path, since a large tree takes time to walk. ## Tracking saved notes The **index** is the source of truth, and the files are its payload. - Store the path **relative** to the support directory, for example `notes/algebra/week3.pdf`, and join it with the resolved directory at runtime. The absolute location of the app's container is not guaranteed to be stable, for example across an iOS reinstall or a restore to a new device, so persisted absolute paths are a common source of "file not found" bugs. - Store the size and a state such as `downloading`, `ready` or `failed`. - On startup, or lazily before opening a note, check that the file exists. If it does not, mark the note as not downloaded and offer to fetch it again. - When the user removes a download, delete the file first and the index row second, or run a cleanup pass that removes orphan files, so a crash between the two steps cannot leave a row pointing at nothing. ## Handling disappearance gracefully Files can vanish without the app's involvement: the user clears storage on Android, the OS purges caches under pressure, or a backup restore brings back the index but not a large file. Each of these should end in the same user-visible state: the note appears as available to download, not as a broken entry. A single "resolve and verify" function that every screen goes through keeps that rule in one place. ## Showing storage usage to the user A settings screen that says "Downloaded notes: 1.2 GB, Cache: 85 MB" builds trust and gives users control. Compute the two numbers separately, one from the index (sum of recorded sizes, cross-checked against the disk occasionally) and one by walking the cache directory. Offer two different buttons, **Clear cache** and **Remove downloads**, with the second asking for confirmation, since it deletes content the user chose to keep. Recalculate after each action and after large downloads finish, not on every build of the screen, because walking a directory tree is I/O that should not run in a widget's build method. ## Mistakes interviewers listen for - Deleting `getApplicationDocumentsDirectory()` or support storage as part of clear cache. - Persisting `File.path` absolute strings in the database. - Deleting a partial file while the download still writes to it. - Treating a missing file as an exception to crash on rather than a normal state.
- Why should a Flutter app store note paths relative to getApplicationSupportDirectory() instead of absolute?The absolute location of the app's container is resolved by the OS and is not guaranteed stable, for example after an iOS reinstall or a restore onto a new device. A relative path joined with a freshly resolved directory keeps working, while a stored absolute path can point at a location that no longer exists.
- How do you keep a download index and the files consistent if the app crashes midway through removing a note?Order the steps so a crash leaves a recoverable state, such as deleting the file before the index row, and run a reconciliation pass at startup that marks rows with missing files as not downloaded and deletes files no row refers to.
saying these in an interview costs you the question
- Clear cache can safely delete the documents directory too.
- Storing the absolute File.path in the database is fine across reinstalls.
- A missing offline file should throw and be reported as a crash.
- Deleting the cache directory itself is the cleanest way to clear it.
- The OS never removes cache files an app is still tracking.