Flutter(Dart)类中Future字段使用及SQLite图片加载卡顿解决
我用SQLite3数据库,有两张关联表:
| Product | Photos |
|---|---|
| id | id |
| name | product_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(); } }
结果应用直接冻结了,想问两个问题:
- 这种场景下正确的实现方式是什么?
- 怎么在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字段时,记住这几个原则:
存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!; } }模型只管数据,别碰Widget
模型的职责是管理数据和业务逻辑,Widget的创建、状态管理这些都交给UI层,不然很容易出现内存泄漏或者状态混乱的问题。批量异步操作别瞎循环,用Future.wait
如果真的需要批量处理异步任务,别像之前那样循环调用不await,用Future.wait()可以控制并发,还能等待所有任务完成:// 比如需要预加载所有图片(不推荐列表场景用) await Future.wait(listOfProducts.map((p) => p.getMainPhotoData()));但列表场景下还是不建议这么做,内存压力太大。
空安全要注意
用late或者可空类型来声明Future字段,确保使用前已经初始化,避免空引用错误。
内容的提问来源于stack exchange,提问作者Oleksandr Lysak

