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

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'];
  }
}

额外优化建议
  1. 封装实体类:可以创建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});
}
  1. 资源安全释放:用try/finally包裹识别逻辑,避免因提前返回导致的资源泄漏:
getImageFromGallery() async {
  try {
    // 原有方法逻辑
  } finally {
    await textRecognizer.close();
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 18:05:56