Implements caching strategies for Flutter apps to improve performance and offline support. Use when retaining app data locally to reduce network requests or speed up startup.
Apply the appropriate caching mechanism based on the data lifecycle and size requirements.
shared_preferences.sqflite, Drift, Hive CE, or Isar).path_provider.FlutterEngine.Design repositories as the single source of truth, combining local databases and remote API clients.
Yield local data immediately for fast UI rendering, then fetch remote data, update the local cache, and yield the fresh data.
Stream<UserProfile> getUserProfile() async* {
// 1. Yield local cache first
final localProfile = await _databaseService.fetchUserProfile();
if (localProfile != null) yield localProfile;
// 2. Fetch remote, update cache, yield fresh data
try {
final remoteProfile = await _apiClientService.getUserProfile();
await _databaseService.updateUserProfile(remoteProfile);
yield remoteProfile;
} catch (e) {
// Handle network failure; UI already has local data
}
}
Determine the write strategy based on data criticality:
Add a synchronized boolean flag to your data models. Run a periodic background task (e.g., via workmanager or a Timer) to push unsynchronized local changes to the server.
Use path_provider to locate the correct directory.
getApplicationDocumentsDirectory() for persistent data.getTemporaryDirectory() for cache data the OS can clear.Future<File> get _localFile async {
final directory = await getApplicationDocumentsDirectory();
return File('${directory.path}/cache.txt');
}
Use sqflite for relational data caching. Always use whereArgs to prevent SQL injection.
Future<void> updateCachedRecord(Record record) async {
final db = await database;
await db.update(
'records',
record.toMap(),
where: 'id = ?',
whereArgs: [record.id], // NEVER use string interpolation here
);
}
Image I/O and decompression are expensive.
cached_network_image package to handle file-system caching of remote images.ImageProvider, override createStream() and resolveStreamForKey() instead of the deprecated resolve() method.ImageCache.maxByteSize no longer automatically expands for large images. If loading images larger than the default cache size, manually increase ImageCache.maxByteSize or subclass ImageCache to implement custom eviction logic.When configuring caching for scrollable widgets (ListView, GridView, Viewport), use the scrollCacheExtent property with a ScrollCacheExtent object. Do not use the deprecated cacheExtent and cacheExtentStyle properties.
// Correct implementation
ListView(
scrollCacheExtent: const ScrollCacheExtent.pixels(500.0),
children: // ...
)
Viewport(
scrollCacheExtent: const ScrollCacheExtent.viewport(0.5),
slivers: // ...
)
operator == on Widget objects. It causes O(N²) behavior during rebuilds.operator == only on leaf widgets (no children) where comparing properties is significantly faster than rebuilding, and the properties rarely change.const constructors to allow the framework to short-circuit rebuilds automatically.To eliminate the non-trivial warm-up time of a FlutterEngine when adding Flutter to an existing Android app, pre-warm and cache the engine.
Application class.FlutterEngineCache.withCachedEngine in the FlutterActivity or FlutterFragment.// 1. Pre-warm in Application class
val flutterEngine = FlutterEngine(this)
flutterEngine.navigationChannel.setInitialRoute("/cached_route")
flutterEngine.dartExecutor.executeDartEntrypoint(DartEntrypoint.createDefault())
// 2. Cache the engine
FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine)
// 3. Use in Activity/Fragment
startActivity(
FlutterActivity.withCachedEngine("my_engine_id").build(this)
)
Note: You cannot set an initial route via the Activity/Fragment builder when using a cached engine. Set the initial route on the engine's navigation channel before executing the Dart entrypoint.
Follow these steps to implement a robust offline-first data layer.
synchronized boolean flag (default false).DatabaseService (SQLite/Hive) with CRUD operations.ApiClientService for network requests.Repository class combining both services.Stream<T> (yield local, fetch remote, update local, yield remote).synchronized flag).synchronized == false.Follow these steps to cache the FlutterEngine for seamless Android integration.
Application class (create one if it doesn't exist and register in AndroidManifest.xml).FlutterEngine.navigationChannel.setInitialRoute().dartExecutor.executeDartEntrypoint().FlutterEngineCache.getInstance().put().FlutterActivity or FlutterFragment to use .withCachedEngine("id").