ListView.builder与Hive数据库配合异常问题求助
问题描述
开发一款宝可梦Go相关应用,功能是接收宝可梦截图,提取属性生成Widget。目前除ListView.builder外其他功能正常:每次新增图片后,之前生成的所有Widget都会变成最后一个新增内容的样子,该问题会持续到应用重启。不想用setState作为解决方案,寻求最优修复方案。
(图片说明:第4张图是重启应用后的正常效果,第5张是宝可梦示例截图,忽略StunkFisk的身高数据。)
相关代码
main.dart
import 'package:flutter/material.dart'; import 'package:pokemon_app/main_screen.dart'; import 'package:hive_flutter/hive_flutter.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Hive.initFlutter(); await Hive.openBox<List<String>>('pokeBox'); runApp( const MyApp(), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: MainScreen(), ); } }
main_screen.dart
import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:pokemon_app/provider/poke_provider.dart'; import 'package:pokemon_app/poke_card/poke_class.dart'; class MainScreen extends StatefulWidget { const MainScreen({super.key}); @override State<MainScreen> createState() => _MainScreenState(); } class _MainScreenState extends State<MainScreen> { late final pokeProvider; @override void initState() { pokeProvider = PokeProvider(); super.initState(); } @override Widget build(BuildContext context) { var pokeBox = Hive.box<List<String>>('pokeBox'); return Scaffold( appBar: AppBar( title: const Text('Main Screen'), actions: [ IconButton( onPressed: () { pokeBox.clear(); setState(() {}); }, icon: const Icon(Icons.delete), ), IconButton( onPressed: pokeProvider.getImageFromGallery, icon: const Icon( Icons.add_outlined, color: Colors.white, ), ), ], ), body: Center( child: ValueListenableBuilder<Box<List<String>>>( valueListenable: pokeBox.listenable(), builder: (context, pokeBox, _) { return pokeBox.isEmpty ? const Center( child: Text(''), ) : ListView.builder( itemCount: pokeBox.length, padding: const EdgeInsetsDirectional.only(bottom: 8), itemBuilder: (context, index) { return PokeCard( singlePokeList: pokeProvider.getFromBox(index), ); }, ); }, ), ), ); } }
poke_provider.dart
import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; import 'package:image_picker/image_picker.dart'; import 'package:image/image.dart' as img; import 'package:path_provider/path_provider.dart'; import 'package:hive_flutter/hive_flutter.dart'; class PokeProvider{ final imagePicker = ImagePicker(); late File? imagePath; late File croppedImageFile; final textRecognizer = TextRecognizer(script: TextRecognitionScript.latin); int x = 0; var pokeBox = Hive.box<List<String>>('pokeBox'); List<String> pokeList = [ 'Name', 'CP', 'Height', 'Weight' ]; String pokeName = 'Name'; String pokeCp = 'cp'; String pokeHeight = 'Height'; String pokeWeight = 'Weight'; getImageFromGallery() async { final dummyImage = await imagePicker.pickImage(source: ImageSource.gallery); if (dummyImage == null) return; final imagePath = File(dummyImage.path); final Image image = Image(image: FileImage(imagePath)); Completer<ui.Image> completer = Completer<ui.Image>(); image.image.resolve(const ImageConfiguration()).addListener( ImageStreamListener( (ImageInfo image, bool _) { completer.complete(image.image); }, ), ); ui.Image info = await completer.future; int width = info.width; int height = info.height; final imageBytes = img.decodeImage(imagePath.readAsBytesSync()); final croppedImage = img.copyCrop(imageBytes!, x: 0, y: (height * 0.35).round(), width: width, height: (height * 0.15).round()); final directory = await getTemporaryDirectory(); final directoryPath = directory.path; final croppedImagePng = img.encodePng(croppedImage); croppedImageFile = await File( '$directoryPath/CroppedImageFile$x.png') .writeAsBytes(croppedImagePng); final croppedInputImage = InputImage.fromFile(croppedImageFile); final inputImage = InputImage.fromFile(imagePath); final RecognizedText recognizedText = await textRecognizer.processImage(inputImage); final RecognizedText croppedRecognizedText = await textRecognizer.processImage(croppedInputImage); for (TextBlock block in croppedRecognizedText.blocks) { for (TextLine line in block.lines) { pokeName = line.text; break; } break; } for (TextBlock block in recognizedText.blocks) { for (TextLine line in block.lines) { if (line.text.contains('kg') || line.text.contains('m') || line.text.contains('CP') || line.text.contains('DG') ) { if (line.text.contains('kg')) { pokeWeight = line.text; } else if (line.text.contains('m')) { pokeHeight = line.text; } else if (line.text.contains('CP') ||line.text.contains('DG')) { pokeCp = line.text; } } } } x++; putToBox(pokeName, pokeCp, pokeWeight, pokeHeight); textRecognizer.close(); } void putToBox(String name, String cp, String weight, String height) { pokeList[0] = name; pokeList[1] = cp; pokeList[2] = weight; pokeList[3] = height; pokeBox.add(pokeList); } List<String> getFromBox(int index) { return pokeBox.getAt(index)!; } }
poke_card.dart
class PokeCard extends StatelessWidget { const PokeCard( {super.key, required this.singlePokeList}); final List<String> singlePokeList; @override Widget build(BuildContext context) { Size screenSize = MediaQuery.of(context).size; return Container( margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( color: Colors.amber.shade100, borderRadius: const BorderRadius.all(Radius.circular(15)), ), width: screenSize.width * 0.55, height: screenSize.height * 0.1, child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.all(12.0), child: Text(singlePokeList[0]), ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Text(singlePokeList[1]), const VerticalDivider(), Text(singlePokeList[2]), const VerticalDivider(), Text(singlePokeList[3]), ], ), ], ), ); } }
问题核心原因
问题出在PokeProvider类的pokeList成员上:
- 这个
pokeList是类的全局成员,每次调用putToBox时,只是修改了同一个List对象的元素值,再把它添加到Hive Box中。 - Hive存储的是List的引用而非独立副本,因此Box里所有条目指向的都是同一个List实例,修改该List内容时,所有条目会同步变化。
- 重启应用后,Hive会将存储的数据反序列化为新的List实例,此时每个条目独立,显示恢复正常。
解决方案
每次添加新数据到Hive时,创建全新的List实例,而非复用同一个List对象。具体修改如下:
修改后的PokeProvider关键代码
class PokeProvider{ final imagePicker = ImagePicker(); late File? imagePath; late File croppedImageFile; final textRecognizer = TextRecognizer(script: TextRecognitionScript.latin); int x = 0; var pokeBox = Hive.box<List<String>>('pokeBox'); String pokeName = 'Name'; String pokeCp = 'cp'; String pokeHeight = 'Height'; String pokeWeight = 'Weight'; // 其他方法保持不变 void putToBox(String name, String cp, String weight, String height) { // 每次创建新的List实例,避免引用复用 final newPokeList = [name, cp, weight, height]; pokeBox.add(newPokeList); } List<String> getFromBox(int index) { // 增加空安全处理,避免空指针异常 return pokeBox.getAt(index) ?? ['Name', 'CP', 'Height', 'Weight']; } }
额外优化建议
- 封装实体类:可以创建
Pokemon实体类替代List存储属性,提升代码可读性与可维护性:
class Pokemon { final String name; final String cp; final String weight; final String height; Pokemon({required this.name, required this.cp, required this.weight, required this.height}); }
- 资源安全释放:用
try/finally包裹识别逻辑,避免因提前返回导致的资源泄漏:
getImageFromGallery() async { try { // 原有方法逻辑 } finally { await textRecognizer.close(); } }
内容的提问来源于stack exchange,提问作者Earalias
相关产品推荐
相关产品推荐

