flutter-caching-data

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Implementing Flutter Caching and Offline-First Architectures

实现Flutter缓存与离线优先架构

Contents

目录

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
    .
根据数据生命周期和大小需求,选用合适的缓存机制。
  • 若存储小型、非关键的UI状态或偏好设置: 使用
    shared_preferences
  • 若存储大型、结构化数据集: 使用本地数据库(通过
    sqflite
    、Drift、Hive CE或Isar操作SQLite)。
  • 若存储二进制数据或大型媒体文件: 通过
    path_provider
    使用文件系统缓存。
  • 若保留用户会话状态(导航、滚动位置): 实现Flutter内置的状态恢复功能,将Element树与引擎同步。
  • 若优化Android初始化速度: 预加载并缓存
    FlutterEngine

Implementing Offline-First Data Synchronization

实现离线优先的数据同步

Design repositories as the single source of truth, combining local databases and remote API clients.
将仓库设计为单一数据源,结合本地数据库与远程API客户端。

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
  }
}
立即返回本地数据以实现快速UI渲染,随后获取远程数据、更新本地缓存并返回最新数据。
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.
根据数据的重要性选择写入策略:
  • 若要求严格与服务器同步(仅在线): 先尝试API调用,仅当调用成功时才更新本地数据库。
  • 若优先保障离线可用性(离线优先): 立即写入本地数据库,再尝试API调用。若调用失败,标记本地记录以便后台同步。

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.
在数据模型中添加
synchronized
布尔标记。通过定期后台任务(如
workmanager
Timer
)将未同步的本地变更推送到服务器。

Managing File System and SQLite Persistence

管理文件系统与SQLite持久化

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');
}
使用
path_provider
定位正确的目录。
  • 使用
    getApplicationDocumentsDirectory()
    存储持久化数据。
  • 使用
    getTemporaryDirectory()
    存储可被系统清理的缓存数据。
dart
Future<File> get _localFile async {
  final directory = await getApplicationDocumentsDirectory();
  return File('${directory.path}/cache.txt');
}

SQLite Persistence

SQLite持久化

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
  );
}
使用
sqflite
进行关系型数据缓存。务必使用
whereArgs
以防止SQL注入。
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

优化UI、滚动与图片缓存

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.
图片I/O与解压缩开销较大。
  • 使用
    cached_network_image
    包处理远程图片的文件系统缓存。
  • 自定义ImageProviders: 若实现自定义
    ImageProvider
    ,请重写
    createStream()
    resolveStreamForKey()
    方法,而非已废弃的
    resolve()
    方法。
  • 缓存大小:
    ImageCache.maxByteSize
    不再自动为大图片扩容。若加载的图片超过默认缓存大小,需手动增大
    ImageCache.maxByteSize
    ,或继承
    ImageCache
    实现自定义淘汰逻辑。

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: // ...
)
为可滚动组件(
ListView
GridView
Viewport
)配置缓存时,使用
scrollCacheExtent
属性搭配
ScrollCacheExtent
对象。请勿使用已废弃的
cacheExtent
cacheExtentStyle
属性。
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.
  • 避免在
    Widget
    对象上重写
    operator ==
    ,这会导致重建时出现O(N²)的性能问题。
  • 例外情况: 仅可在无子组件的叶子Widget上重写
    operator ==
    ,且仅当比较属性的速度远快于重建、且属性极少变更时才适用。
  • 优先使用
    const
    构造函数,让框架自动跳过不必要的重建。

Caching the FlutterEngine (Android)

缓存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.
在现有Android应用中集成Flutter时,为消除
FlutterEngine
的显著预热时间,可预加载并缓存引擎。
  1. Application
    类中实例化并预加载引擎。
  2. 将其存储到
    FlutterEngineCache
    中。
  3. FlutterActivity
    FlutterFragment
    中通过
    withCachedEngine
    获取引擎。
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)
)
注意:使用缓存引擎时,无法通过Activity/Fragment的构建器设置初始路由。需在执行Dart入口点前,通过引擎的navigationChannel设置初始路由。

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).
遵循以下步骤构建健壮的离线优先数据层。
  • 任务进度:
    • 定义包含
      synchronized
      布尔标记(默认
      false
      )的数据模型。
    • 实现本地
      DatabaseService
      (基于SQLite/Hive),包含CRUD操作。
    • 实现用于网络请求的远程
      ApiClientService
    • 创建结合两种服务的
      Repository
      类。
    • 实现返回
      Stream<T>
      的读取方法(先返回本地数据,再获取远程数据、更新本地、返回远程数据)。
    • 实现写入方法(先写入本地,再尝试远程调用,更新
      synchronized
      标记)。
    • 实现后台同步函数,处理
      synchronized == false
      的记录。
    • 运行验证器 -> 检查错误 -> 修复(通过禁用网络测试离线行为)。

Workflow: Pre-warming the Android FlutterEngine

工作流程:预加载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).
遵循以下步骤缓存FlutterEngine,实现流畅的Android集成。
  • 任务进度:
    • 找到Android的
      Application
      类(若不存在则创建并在
      AndroidManifest.xml
      中注册)。
    • 实例化新的
      FlutterEngine
    • (可选)通过
      navigationChannel.setInitialRoute()
      设置初始路由。
    • 通过
      dartExecutor.executeDartEntrypoint()
      执行Dart入口点。
    • 将引擎存储到
      FlutterEngineCache.getInstance().put()
      中。
    • 修改目标
      FlutterActivity
      FlutterFragment
      ,使其使用
      .withCachedEngine("id")
    • 运行验证器 -> 检查错误 -> 修复(验证切换过程中是否出现空白屏幕)。