flutter-caching-and-performance
Goal
Implements advanced caching, offline-first data persistence, and performance optimization strategies in Flutter applications. Evaluates application requirements to select and integrate the appropriate local caching mechanism (in-memory, persistent, file system, or on-device databases). Configures Android-specific FlutterEngine caching to minimize initialization latency. Optimizes widget rendering, image caching, and scrolling performance while adhering to current Flutter API standards and avoiding expensive rendering operations.
Instructions
- Evaluate and Select Caching Strategy (Decision Logic)
Analyze the user's data retention requirements using the following decision tree to select the appropriate caching mechanism:
-
Is the data temporary and only needed for the current session?
-
Yes: Use In-memory caching.
-
Is the data small, simple key-value pairs (e.g., user preferences)?
-
Yes: Use shared_preferences .
-
Is the data large, relational, or requires complex querying?
-
Yes: Use On-device databases (e.g., sqflite ). Proceed to Step 3.
-
Is the data large binary files, custom documents, or JSON blobs?
-
Yes: Use File system caching (path_provider ). Proceed to Step 2.
-
Is the data network images?
-
Yes: Use Image caching (cached_network_image or custom ImageCache ). Proceed to Step 6.
-
Is the goal to reduce Android Flutter UI warm-up time?
-
Yes: Use FlutterEngine caching. Proceed to Step 5.
STOP AND ASK THE USER: "Based on your requirements, which data type and size are we handling? Should I implement SQLite, File System caching, or a different strategy?"
- Implement File System Caching
When shared_preferences is insufficient for larger data, use path_provider and dart:io to persist data to the device's hard drive.
import 'dart:io'; import 'package:path_provider/path_provider.dart';
class FileCacheService { Future<String> get _localPath async { // Use getTemporaryDirectory() for system-cleared cache // Use getApplicationDocumentsDirectory() for persistent data final directory = await getApplicationDocumentsDirectory(); return directory.path; }
Future<File> get _localFile async { final path = await _localPath; return File('$path/cached_data.json'); }
Future<File> writeData(String data) async { final file = await _localFile; return file.writeAsString(data); }
Future<String?> readData() async { try { final file = await _localFile; return await file.readAsString(); } catch (e) { return null; // Cache miss } } }
- Implement SQLite Persistence
For large datasets requiring improved performance over simple files, implement an on-device database using sqflite .
import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart';
class DatabaseService { late Database _database;
Future<void> initDB() async { _database = await openDatabase( join(await getDatabasesPath(), 'app_cache.db'), onCreate: (db, version) { return db.execute( 'CREATE TABLE cache_data(id INTEGER PRIMARY KEY, key TEXT, payload TEXT)', ); }, version: 1, ); }
Future<void> insertCache(String key, String payload) async { await _database.insert( 'cache_data', {'key': key, 'payload': payload}, conflictAlgorithm: ConflictAlgorithm.replace, ); }
Future<String?> getCache(String key) async { final List<Map<String, Object?>> maps = await _database.query( 'cache_data', where: 'key = ?', whereArgs: [key], // MUST use whereArgs to prevent SQL injection ); if (maps.isNotEmpty) { return maps.first['payload'] as String; } return null; } }
- Implement Offline-First Repository (Stream-based)
Combine local caching and remote fetching. Yield the cached data first (cache hit), then fetch from the network, update the cache, and yield the fresh data.
Stream<UserProfile> getUserProfile() async* { // 1. Check local cache final localData = await _databaseService.getCache('user_profile'); if (localData != null) { yield UserProfile.fromJson(localData); }
// 2. Fetch remote data try { final remoteData = await _apiClient.fetchUserProfile(); // 3. Update cache await _databaseService.insertCache('user_profile', remoteData.toJson()); // 4. Yield fresh data yield remoteData; } catch (e) { // Handle network failure; local data has already been yielded } }
- Implement FlutterEngine Caching (Android)
To minimize Flutter's initialization time when adding Flutter screens to an Android app, pre-warm and cache the FlutterEngine .
Pre-warm in Application class (Kotlin):
class MyApplication : Application() { lateinit var flutterEngine : FlutterEngine override fun onCreate() { super.onCreate() flutterEngine = FlutterEngine(this) // Optional: Configure initial route before executing entrypoint flutterEngine.navigationChannel.setInitialRoute("/cached_route"); flutterEngine.dartExecutor.executeDartEntrypoint( DartExecutor.DartEntrypoint.createDefault() ) FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine) } }
Consume in Activity/Fragment (Kotlin):
// For Activity startActivity( FlutterActivity .withCachedEngine("my_engine_id") .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent) .build(this) )
// For Fragment val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id") .renderMode(FlutterView.RenderMode.texture) .shouldAttachEngineToActivity(false) .build()
- Optimize Image and Scroll Caching
Apply strict constraints to image caching and scrolling to prevent GPU memory bloat and layout passes.
ImageCache Validation: Verify cache hits without triggering loads using containsKey .
class CustomImageCache extends ImageCache { @override bool containsKey(Object key) { // Check if cache is tracking this key return super.containsKey(key); } }
ScrollCacheExtent Implementation: Use the strongly-typed ScrollCacheExtent object for scrolling widgets (replaces deprecated cacheExtent and cacheExtentStyle ).
ListView( // Use ScrollCacheExtent.pixels for pixel-based caching scrollCacheExtent: const ScrollCacheExtent.pixels(500.0), children: [ ... ], )
Viewport( // Use ScrollCacheExtent.viewport for fraction-based caching scrollCacheExtent: const ScrollCacheExtent.viewport(0.5), slivers: [ ... ], )
- Validate-and-Fix Performance
Review the generated UI code for performance pitfalls.
-
Check for saveLayer() triggers: Ensure Opacity , ShaderMask , ColorFilter , and Clip.antiAliasWithSaveLayer are only used when absolutely necessary. Replace Opacity with semitransparent colors or FadeInImage where possible.
-
Check operator == overrides: Ensure operator == is NOT overridden on Widget objects unless they are leaf widgets whose properties rarely change.
-
Check Intrinsic Passes: Ensure ListView and GridView use lazy builder methods (ListView.builder ) and avoid intrinsic layout passes by setting fixed sizes where possible.
Constraints
-
SQL Injection Prevention: ALWAYS use whereArgs in sqflite queries. NEVER use string interpolation for SQL where clauses.
-
Engine Lifecycle: When using a cached FlutterEngine in Android, remember it outlives the Activity /Fragment . Explicitly call FlutterEngine.destroy() when it is no longer needed to clear resources.
-
Image Caching Limits: Raster cache entries are expensive to construct and use significant GPU memory. Only cache images when absolutely necessary. Do not artificially inflate ImageCache.maxByteSize .
-
Widget Equality: Do not override operator == on widgets to force caching, as this degrades performance to O(N²). Rely on const constructors instead.
-
Scroll Extents: NEVER use the deprecated cacheExtent (double) or cacheExtentStyle . ALWAYS use the ScrollCacheExtent object.
-
Web Workers: If targeting Flutter Web, remember that Dart isolates are not supported. Do not generate isolate-based background parsing for web targets.