Files
tools-show/.agents/skills/flutter-caching-data/SKILL.md

169 lines
7.7 KiB
Markdown
Raw Normal View History

2026-04-11 20:46:55 +08:00
---
name: "flutter-caching-data"
description: "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."
metadata:
model: "models/gemini-3.1-pro-preview"
last_modified: "Thu, 12 Mar 2026 22:19:54 GMT"
---
# Implementing Flutter Caching and Offline-First Architectures
## Contents
- [Selecting a Caching Strategy](#selecting-a-caching-strategy)
- [Implementing Offline-First Data Synchronization](#implementing-offline-first-data-synchronization)
- [Managing File System and SQLite Persistence](#managing-file-system-and-sqlite-persistence)
- [Optimizing UI, Scroll, and Image Caching](#optimizing-ui-scroll-and-image-caching)
- [Caching the FlutterEngine (Android)](#caching-the-flutterengine-android)
- [Workflows](#workflows)
## Selecting a Caching Strategy
Apply the appropriate caching mechanism based on the data lifecycle and size requirements.
* **If storing small, non-critical UI states or preferences:** Use `shared_preferences`.
* **If storing large, structured datasets:** Use on-device databases (SQLite via `sqflite`, Drift, Hive CE, or Isar).
* **If storing binary data or large media:** Use file system caching via `path_provider`.
* **If retaining user session state (navigation, scroll positions):** Implement Flutter's built-in state restoration to sync the Element tree with the engine.
* **If optimizing Android initialization:** Pre-warm and cache the `FlutterEngine`.
## Implementing Offline-First Data Synchronization
Design repositories as the single source of truth, combining local databases and remote API clients.
### Read Operations (Stream Approach)
Yield local data immediately for fast UI rendering, then fetch remote data, update the local cache, and yield the fresh data.
```dart
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
}
}
```
### Write Operations
Determine the write strategy based on data criticality:
* **If strict server synchronization is required (Online-only):** Attempt the API call first. Only update the local database if the API call succeeds.
* **If offline availability is prioritized (Offline-first):** Write to the local database immediately. Attempt the API call. If the API call fails, flag the local record for background synchronization.
### Background Synchronization
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.
## Managing File System and SQLite Persistence
### File System Caching
Use `path_provider` to locate the correct directory.
* Use `getApplicationDocumentsDirectory()` for persistent data.
* Use `getTemporaryDirectory()` for cache data the OS can clear.
```dart
Future<File> get _localFile async {
final directory = await getApplicationDocumentsDirectory();
return File('${directory.path}/cache.txt');
}
```
### SQLite Persistence
Use `sqflite` for relational data caching. Always use `whereArgs` to prevent SQL injection.
```dart
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
);
}
```
## Optimizing UI, Scroll, and Image Caching
### Image Caching
Image I/O and decompression are expensive.
* Use the `cached_network_image` package to handle file-system caching of remote images.
* **Custom ImageProviders:** If implementing a custom `ImageProvider`, override `createStream()` and `resolveStreamForKey()` instead of the deprecated `resolve()` method.
* **Cache Sizing:** The `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.
### Scroll Caching
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.
```dart
// Correct implementation
ListView(
scrollCacheExtent: const ScrollCacheExtent.pixels(500.0),
children: // ...
)
Viewport(
scrollCacheExtent: const ScrollCacheExtent.viewport(0.5),
slivers: // ...
)
```
### Widget Caching
* Avoid overriding `operator ==` on `Widget` objects. It causes O(N²) behavior during rebuilds.
* **Exception:** You may override `operator ==` *only* on leaf widgets (no children) where comparing properties is significantly faster than rebuilding, and the properties rarely change.
* Prefer using `const` constructors to allow the framework to short-circuit rebuilds automatically.
## Caching the FlutterEngine (Android)
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.
1. Instantiate and pre-warm the engine in the `Application` class.
2. Store it in the `FlutterEngineCache`.
3. Retrieve it using `withCachedEngine` in the `FlutterActivity` or `FlutterFragment`.
```kotlin
// 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.*
## Workflows
### Workflow: Implementing an Offline-First Repository
Follow these steps to implement a robust offline-first data layer.
- [ ] **Task Progress:**
- [ ] Define the data model with a `synchronized` boolean flag (default `false`).
- [ ] Implement the local `DatabaseService` (SQLite/Hive) with CRUD operations.
- [ ] Implement the remote `ApiClientService` for network requests.
- [ ] Create the `Repository` class combining both services.
- [ ] Implement the read method returning a `Stream<T>` (yield local, fetch remote, update local, yield remote).
- [ ] Implement the write method (write local, attempt remote, update `synchronized` flag).
- [ ] Implement a background sync function to process records where `synchronized == false`.
- [ ] Run validator -> review errors -> fix (Test offline behavior by disabling network).
### Workflow: Pre-warming the Android FlutterEngine
Follow these steps to cache the FlutterEngine for seamless Android integration.
- [ ] **Task Progress:**
- [ ] Locate the Android `Application` class (create one if it doesn't exist and register in `AndroidManifest.xml`).
- [ ] Instantiate a new `FlutterEngine`.
- [ ] (Optional) Set the initial route via `navigationChannel.setInitialRoute()`.
- [ ] Execute the Dart entrypoint via `dartExecutor.executeDartEntrypoint()`.
- [ ] Store the engine in `FlutterEngineCache.getInstance().put()`.
- [ ] Update the target `FlutterActivity` or `FlutterFragment` to use `.withCachedEngine("id")`.
- [ ] Run validator -> review errors -> fix (Verify no blank screen appears during transition).