flutter-caching

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

flutter-caching-and-performance

Flutter缓存与性能

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.
在Flutter应用中实现高级缓存、离线优先的数据持久化以及性能优化策略。评估应用需求以选择并集成合适的本地缓存机制(内存缓存、持久化缓存、文件系统或设备端数据库)。配置Android专属的FlutterEngine缓存以最小化初始化延迟。在遵循当前Flutter API标准、避免高开销渲染操作的同时,优化Widget渲染、图片缓存和滚动性能。

Instructions

操作指南

1. Evaluate and Select Caching Strategy (Decision Logic)

1. 评估并选择缓存策略(决策逻辑)

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?"
使用以下决策树分析用户的数据保留需求,以选择合适的缓存机制:
  • 数据是否为临时数据,仅当前会话需要使用?
    • 是: 使用内存缓存
  • 数据是否为小型、简单的键值对(例如用户偏好设置)?
    • 是: 使用**
      shared_preferences
      **。
  • 数据是否体量较大、为关系型数据,或需要复杂查询?
    • 是: 使用设备端数据库(例如
      sqflite
      ),前往步骤3。
  • 数据是否为大型二进制文件、自定义文档或JSON blob?
    • 是: 使用文件系统缓存
      path_provider
      ),前往步骤2。
  • 数据是否为网络图片?
    • 是: 使用图片缓存
      cached_network_image
      或自定义
      ImageCache
      ),前往步骤6。
  • 目标是否为减少Android端Flutter UI预热时间?
    • 是: 使用FlutterEngine缓存,前往步骤5。
请停止并询问用户: "根据您的需求,我们要处理的数据类型和大小是什么?我应该实现SQLite、文件系统缓存还是其他策略?"

2. Implement File System Caching

2. 实现文件系统缓存

When
shared_preferences
is insufficient for larger data, use
path_provider
and
dart:io
to persist data to the device's hard drive.
dart
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
    }
  }
}
shared_preferences
不足以存储更大体量的数据时,使用
path_provider
dart:io
将数据持久化到设备硬盘中。
dart
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
    }
  }
}

3. Implement SQLite Persistence

3. 实现SQLite持久化

For large datasets requiring improved performance over simple files, implement an on-device database using
sqflite
.
dart
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;
  }
}
对于相比简单文件需要更高性能的大型数据集,使用
sqflite
实现设备端数据库。
dart
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;
  }
}

4. Implement Offline-First Repository (Stream-based)

4. 实现离线优先仓库(基于Stream)

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.
dart
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
  }
}
结合本地缓存和远程拉取。首先返回缓存数据(缓存命中),然后从网络拉取数据,更新缓存后返回最新数据。
dart
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
  }
}

5. Implement FlutterEngine Caching (Android)

5. 实现FlutterEngine缓存(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):
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):
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()
为了在Android应用中添加Flutter页面时最小化Flutter的初始化时间,可预热并缓存
FlutterEngine
在Application类中预热(Kotlin):
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)
  }
}
在Activity/Fragment中使用(Kotlin):
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()

6. Optimize Image and Scroll Caching

6. 优化图片与滚动缓存

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
.
dart
class CustomImageCache extends ImageCache {
  
  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
).
dart
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: [ ... ],
)
对图片缓存和滚动行为施加严格约束,避免GPU内存膨胀和多余布局遍历。
ImageCache校验: 使用
containsKey
验证缓存命中,无需触发加载。
dart
class CustomImageCache extends ImageCache {
  
  bool containsKey(Object key) {
    // Check if cache is tracking this key
    return super.containsKey(key); 
  }
}
ScrollCacheExtent实现: 为滚动Widget使用强类型
ScrollCacheExtent
对象(替代已废弃的
cacheExtent
cacheExtentStyle
)。
dart
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: [ ... ],
)

7. Validate-and-Fix Performance

7. 校验并修复性能问题

Review the generated UI code for performance pitfalls.
  1. 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.
  2. Check
    operator ==
    overrides:
    Ensure
    operator ==
    is NOT overridden on
    Widget
    objects unless they are leaf widgets whose properties rarely change.
  3. Check Intrinsic Passes: Ensure
    ListView
    and
    GridView
    use lazy builder methods (
    ListView.builder
    ) and avoid intrinsic layout passes by setting fixed sizes where possible.
检查生成的UI代码是否存在性能陷阱:
  1. 检查
    saveLayer()
    触发场景:
    确保仅在绝对必要时使用
    Opacity
    ShaderMask
    ColorFilter
    Clip.antiAliasWithSaveLayer
    。尽可能用半透明颜色或
    FadeInImage
    替代
    Opacity
  2. 检查
    operator ==
    重写:
    确保不要在
    Widget
    对象上重写
    operator ==
    ,除非它们是属性极少变化的叶子节点Widget。
  3. 检查内联布局遍历: 确保
    ListView
    GridView
    使用懒加载构建方法(
    ListView.builder
    ),尽可能设置固定尺寸避免内联布局遍历。

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.
  • SQL注入预防:
    sqflite
    查询中始终使用
    whereArgs
    ,绝不要在SQL
    where
    子句中使用字符串插值。
  • 引擎生命周期: 在Android中使用缓存的
    FlutterEngine
    时,需注意它的生命周期长于
    Activity
    /
    Fragment
    。当不再需要时需显式调用
    FlutterEngine.destroy()
    释放资源。
  • 图片缓存限制: 栅格缓存条目构建成本高,会占用大量GPU内存。仅在绝对必要时缓存图片,不要人为调高
    ImageCache.maxByteSize
  • Widget相等性判断: 不要通过重写Widget的
    operator ==
    强制缓存,这会将性能降低到O(N²),应优先使用
    const
    构造函数。
  • 滚动缓存范围: 绝不要使用已废弃的
    cacheExtent
    (double类型)或
    cacheExtentStyle
    ,始终使用
    ScrollCacheExtent
    对象。
  • Web Workers: 如果目标是Flutter Web,需注意Dart
    isolates
    不受支持,不要为Web端生成基于isolate的后台解析逻辑。