You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter(Dart)类中Future字段使用及SQLite图片加载卡顿解决

问题描述

我用SQLite3数据库,有两张关联表:

ProductPhotos
idid
nameproduct_id
data(base64)

现有获取产品列表的异步方法:

Future<List<Product>?> getAllProducts() async {
   textQuery = "SELECT * FROM Product limit 100"; 
   var res = await db!.rawQuery(textQuery);
   List<Product>? list = res.isNotEmpty ? res.map((c) => Product.fromJson(c)).toList() : [];
   return list;
}

Product模型定义如下:

class Product {
  late int id;
  late String name;

  Product({
    required this.id,
    required this.name,
  });
  factory Product.fromJson(Map<String, dynamic> json) => Product(
        id: json["id"],
        name: json["name"],
  );
}

我通过FutureBuilder展示产品列表,现在需要显示对应产品的图片,于是尝试在Product模型里加了这些代码:

late Image _image;

Image imageFromBase64String(String base64String) {
  return Image.memory(base64Decode(base64String));
}

void initAsync() async {
  Photo photo = await DBProvider.db.getMainPhoto(id);
  _image = imageFromBase64String(photo.data);
}

然后在Widget里这么用:

Future<bool> _getList() async {
  listOfProducts = (await DBProvider.db.getAllProducts(selectedCategory))!;
  for (Product product in listOfProducts) {
    product.initAsync();
  }
}

结果应用直接冻结了,想问两个问题:

  1. 这种场景下正确的实现方式是什么?
  2. 怎么在Flutter(Dart)的类里合理使用Future字段?

解决方案

一、当前场景的正确实现

你这代码冻结的核心问题有两个:

  • 循环调用initAsync()却不await,导致几十上百个数据库请求同时触发,瞬间占满资源;
  • 直接在模型里存Image实例,相当于一次性加载所有产品的图片,内存直接爆了,UI自然卡到冻结。

正确的思路是让每个列表项自己异步加载对应图片,延迟加载而非一次性全加载,具体步骤如下:

1. 调整Product模型

别在模型里存Image,改成提供获取图片数据的方法就行,把UI相关的逻辑交给Widget层:

class Product {
  late int id;
  late String name;

  Product({
    required this.id,
    required this.name,
  });

  factory Product.fromJson(Map<String, dynamic> json) => Product(
        id: json["id"],
        name: json["name"],
      );

  // 只负责获取图片的base64数据,不处理Image创建
  Future<String?> getMainPhotoData() async {
    final photo = await DBProvider.db.getMainPhoto(id);
    return photo?.data;
  }
}

2. 列表项单独处理图片加载

在每个ProductItem里用FutureBuilder单独处理这一项的图片加载,这样只有当列表项出现在屏幕上时才会触发图片请求:

class ProductItem extends StatelessWidget {
  final Product product;

  const ProductItem({super.key, required this.product});

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: FutureBuilder<String?>(
        future: product.getMainPhotoData(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            // 加载中的占位符,用个小圈圈或者默认图都行
            return const SizedBox(
              width: 50,
              height: 50,
              child: Center(child: CircularProgressIndicator(size: 20)),
            );
          } else if (snapshot.hasData && snapshot.data != null) {
            return Image.memory(
              base64Decode(snapshot.data!),
              width: 50,
              height: 50,
              fit: BoxFit.cover,
            );
          } else {
            // 没有图片时的占位符
            return const Icon(Icons.image_not_supported, size: 50);
          }
        },
      ),
      title: Text(product.name),
    );
  }
}

3. 优化产品列表加载逻辑

原来的_getList()不用再循环调用initAsync()了,直接获取产品列表就行:

Future<void> _getList() async {
  listOfProducts = (await DBProvider.db.getAllProducts(selectedCategory)) ?? [];
}

然后用FutureBuilder渲染整个列表:

FutureBuilder<void>(
  future: _getList(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(child: CircularProgressIndicator());
    }
    return ListView.builder(
      itemCount: listOfProducts.length,
      itemBuilder: (context, index) {
        return ProductItem(product: listOfProducts[index]);
      },
    );
  },
)

额外优化:减少数据库请求

如果产品数量多,建议直接用联表查询一次性拿到产品和对应主图的数据,避免每个列表项都单独查一次数据库:

SELECT p.id, p.name, ph.data 
FROM Product p 
LEFT JOIN Photos ph ON p.id = ph.product_id 
WHERE ph.id = (SELECT MIN(id) FROM Photos WHERE product_id = p.id)
LIMIT 100;

这样getAllProducts()可以直接返回带图片数据的Product对象,性能会好很多。

二、Dart类中合理使用Future字段的技巧

在类里用Future字段时,记住这几个原则:

  1. 存Future本身,而不是存Future的结果
    这样可以避免重复发起请求,比如在Product类里缓存图片请求的Future:

    class Product {
      late int id;
      late String name;
      Future<String?>? _photoDataFuture;
    
      Future<String?> getMainPhotoData() {
        // 如果已经发起过请求,直接返回之前的Future,避免重复查数据库
        if (_photoDataFuture == null) {
          _photoDataFuture = DBProvider.db.getMainPhoto(id);
        }
        return _photoDataFuture!;
      }
    }
    
  2. 模型只管数据,别碰Widget
    模型的职责是管理数据和业务逻辑,Widget的创建、状态管理这些都交给UI层,不然很容易出现内存泄漏或者状态混乱的问题。

  3. 批量异步操作别瞎循环,用Future.wait
    如果真的需要批量处理异步任务,别像之前那样循环调用不await,用Future.wait()可以控制并发,还能等待所有任务完成:

    // 比如需要预加载所有图片(不推荐列表场景用)
    await Future.wait(listOfProducts.map((p) => p.getMainPhotoData()));
    

    但列表场景下还是不建议这么做,内存压力太大。

  4. 空安全要注意
    用late或者可空类型来声明Future字段,确保使用前已经初始化,避免空引用错误。


内容的提问来源于stack exchange,提问作者Oleksandr Lysak

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 16:44:57