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

Flutter:三个FutureBuilder仅最后一个正常,其余数据重复求助

问题描述

在SingleChildScrollView中展示三个独立的、带有ListView.separated的FutureBuilder组件,通过Provider从SQFLite数据库获取数据,但实际运行时只有最后一个FutureBuilder能正常工作,前两个组件的输出内容与最后一个完全一致。

问题代码

class SelectCategoryPage extends StatefulWidget {
  const SelectCategoryPage({Key? key}) : super(key: key);

  @override
  State<SelectCategoryPage> createState() => _SelectCategoryPageState();
}

class _SelectCategoryPageState extends State<SelectCategoryPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: kBackgroundColor,
      appBar: _appBar(),
      body: _body(context),
      bottomNavigationBar: _bottomNavigationBar(),
    );
  }

  AppBar _appBar() {
    return AppBar(
      elevation: 0,
      backgroundColor: Colors.white,
      title: Text(
        'Select Category',
        style: appBarHeaderTStyle,
      ),
      iconTheme: const IconThemeData(color: Colors.black),
      actions: [
        Consumer<SelectCategoryViewModel>(
          builder: (context, provider, child) {
            return TextButton(
              child: Text(
                provider.getEditOptionData ? 'Save' : 'Edit',
                style: textButtonTStyle,
              ),
              onPressed: () {
                provider.toggleEditButton();
              },
            );
          },
        ),
      ],
    );
  }

  Widget _body(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 20, left: 24, right: 24),
      child: Column(
        children: [
          Expanded(
            child: SingleChildScrollView(
              controller: null,
              physics: const BouncingScrollPhysics(),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const SizedBox(height: 12),
                  Text(
                    'Expense',
                    style: selectCategoryHeaderTStyle,
                  ),
                  const SizedBox(height: 16),
                  _expenseCategory(context),
                  const SizedBox(height: 24),
                  Text(
                    'Income',
                    style: selectCategoryHeaderTStyle,
                  ),
                  const SizedBox(height: 16),
                  _incomeCategory(context),
                  const SizedBox(height: 24),
                  Text(
                    'Other',
                    style: selectCategoryHeaderTStyle,
                  ),
                  const SizedBox(height: 16),
                  _otherCategory(context),
                  const SizedBox(height: 20),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  FutureBuilder<void> _expenseCategory(BuildContext context) {
    return FutureBuilder(
      future: Provider.of<SelectCategoryViewModel>(
        context,
        listen: false,
      ).selectCategoryByExpenseType(),
      builder: (context, snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.waiting:
            return const Center(
              child: CircularProgressIndicator(),
            );
          case ConnectionState.done:
            return Consumer<SelectCategoryViewModel>(
              child: const Center(child: Text('No Data')),
              builder: (context, expenseProvider, child) => expenseProvider
                      .data.isEmpty
                  ? child!
                  : ListView.separated(
                      shrinkWrap: true,
                      itemCount: expenseProvider.data.length,
                      physics: const BouncingScrollPhysics(),
                      separatorBuilder: (BuildContext context, int index) {
                        return const SizedBox(height: 16);
                      },
                      itemBuilder: (BuildContext context, int index) {
                        return SelectCategoryCard(
                          id: expenseProvider.data[index].id,
                          coloredIcon: expenseProvider.data[index].categoryIcon,
                          title: expenseProvider.data[index].categoryName,
                          isOptionDotsVisitable:
                              expenseProvider.getEditOptionData,
                        );
                      },
                    ),
            );
          default:
            return Container();
        }
      },
    );
  }

  FutureBuilder<void> _incomeCategory(BuildContext context) {
    return FutureBuilder(
      future: Provider.of<SelectCategoryViewModel>(
        context,
        listen: false,
      ).selectCategoryByIncomeType(),
      builder: (context, snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.waiting:
            return const Center(
              child: CircularProgressIndicator(),
            );
          case ConnectionState.done:
            return Consumer<SelectCategoryViewModel>(
              child: const Center(child: Text('No Data')),
              builder: (context, incomeProvider, child) => incomeProvider
                      .data.isEmpty
                  ? child!
                  : ListView.separated(
                      shrinkWrap: true,
                      itemCount: incomeProvider.data.length,
                      physics: const BouncingScrollPhysics(),
                      separatorBuilder: (BuildContext context, int index) {
                        return const SizedBox(height: 16);
                      },
                      itemBuilder: (BuildContext context, int index) {
                        return SelectCategoryCard(
                          id: incomeProvider.data[index].id,
                          coloredIcon: incomeProvider.data[index].categoryIcon,
                          title: incomeProvider.data[index].categoryName,
                          isOptionDotsVisitable:
                              incomeProvider.getEditOptionData,
                        );
                      },
                    ),
            );
          default:
            return Container();
        }
      },
    );
  }

  FutureBuilder<void> _otherCategory(BuildContext context) {
    return FutureBuilder(
      future: Provider.of<SelectCategoryViewModel>(
        context,
        listen: false,
      ).selectCategoryByOtherType(),
      builder: (context, snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.waiting:
            return const Center(
              child: CircularProgressIndicator(),
            );
          case ConnectionState.done:
            return Consumer<SelectCategoryViewModel>(
              child: const Center(child: Text('No Data')),
              builder: (context, otherProvider, child) => otherProvider
                      .data.isEmpty
                  ? child!
                  : ListView.separated(
                      shrinkWrap: true,
                      itemCount: otherProvider.data.length,
                      physics: const BouncingScrollPhysics(),
                      separatorBuilder: (BuildContext context, int index) {
                        return const SizedBox(height: 16);
                      },
                      itemBuilder: (BuildContext context, int index) {
                        return SelectCategoryCard(
                          id: otherProvider.data[index].id,
                          coloredIcon: otherProvider.data[index].categoryIcon,
                          title: otherProvider.data[index].categoryName,
                          isOptionDotsVisitable:
                              otherProvider.getEditOptionData,
                        );
                      },
                    ),
            );
          default:
            return Container();
        }
      },
    );
  }

  Widget _bottomNavigationBar() {
    return CustomBottomAppBar(
      buttonText: '+ Add New Category',
      onTapEvent: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => const CreateNewCategoryPage(),
          ),
        );
      },
    );
  }
}

解决方案

问题根源

你的SelectCategoryViewModel中应该只用了一个data变量存储查询结果,三个查询方法(selectCategoryByExpenseType、selectCategoryByIncomeType、selectCategoryByOtherType)会异步覆盖这个变量的值。最后执行完成的方法会把data设置为自己的结果,导致所有Consumer都读取到最后一次赋值的内容。

修复步骤

1. 修改ViewModel,拆分数据存储

在ViewModel中为三种分类分别定义独立的列表变量,避免数据覆盖:

class SelectCategoryViewModel extends ChangeNotifier {
  // 拆分三个独立的数据列表
  List<Category> expenseData = [];
  List<Category> incomeData = [];
  List<Category> otherData = [];
  
  bool _editOptionData = false;
  bool get getEditOptionData => _editOptionData;

  void toggleEditButton() {
    _editOptionData = !_editOptionData;
    notifyListeners();
  }

  // 查询支出分类,结果存入expenseData
  Future<void> selectCategoryByExpenseType() async {
    expenseData = await dbHelper.getCategoriesByType('expense');
    notifyListeners();
  }

  // 查询收入分类,结果存入incomeData
  Future<void> selectCategoryByIncomeType() async {
    incomeData = await dbHelper.getCategoriesByType('income');
    notifyListeners();
  }

  // 查询其他分类,结果存入otherData
  Future<void> selectCategoryByOtherType() async {
    otherData = await dbHelper.getCategoriesByType('other');
    notifyListeners();
  }
}

2. 更新页面中的FutureBuilder组件

每个FutureBuilder对应的Consumer读取各自专属的数据列表,同时关闭内层ListView的独立滚动(避免和外层SingleChildScrollView冲突):

支出分类组件

FutureBuilder<void> _expenseCategory(BuildContext context) {
  return FutureBuilder(
    future: Provider.of<SelectCategoryViewModel>(context, listen: false)
        .selectCategoryByExpenseType(),
    builder: (context, snapshot) {
      switch (snapshot.connectionState) {
        case ConnectionState.waiting:
          return const Center(child: CircularProgressIndicator());
        case ConnectionState.done:
          return Consumer<SelectCategoryViewModel>(
            child: const Center(child: Text('No Data')),
            builder: (context, provider, child) => provider.expenseData.isEmpty
                ? child!
                : ListView.separated(
                    shrinkWrap: true,
                    physics: const NeverScrollableScrollPhysics(), // 禁用内层滚动
                    itemCount: provider.expenseData.length,
                    separatorBuilder: (_, __) => const SizedBox(height: 16),
                    itemBuilder: (_, index) {
                      final category = provider.expenseData[index];
                      return SelectCategoryCard(
                        id: category.id,
                        coloredIcon: category.categoryIcon,
                        title: category.categoryName,
                        isOptionDotsVisitable: provider.getEditOptionData,
                      );
                    },
                  ),
          );
        default:
          return Container();
      }
    },
  );
}

收入分类组件

FutureBuilder<void> _incomeCategory(BuildContext context) {
  return FutureBuilder(
    future: Provider.of<SelectCategoryViewModel>(context, listen: false)
        .selectCategoryByIncomeType(),
    builder: (context, snapshot) {
      switch (snapshot.connectionState) {
        case ConnectionState.waiting:
          return const Center(child: CircularProgressIndicator());
        case ConnectionState.done:
          return Consumer<SelectCategoryViewModel>(
            child: const Center(child: Text('No Data')),
            builder: (context, provider, child) => provider.incomeData.isEmpty
                ? child!
                : ListView.separated(
                    shrinkWrap: true,
                    physics: const NeverScrollableScrollPhysics(),
                    itemCount: provider.incomeData.length,
                    separatorBuilder: (_, __) => const SizedBox(height: 16),
                    itemBuilder: (_, index) {
                      final category = provider.incomeData[index];
                      return SelectCategoryCard(
                        id: category.id,
                        coloredIcon: category.categoryIcon,
                        title: category.categoryName,
                        isOptionDotsVisitable: provider.getEditOptionData,
                      );
                    },
                  ),
          );
        default:
          return Container();
      }
    },
  );
}

其他分类组件

FutureBuilder<void> _otherCategory(BuildContext context) {
  return FutureBuilder(
    future: Provider.of<SelectCategoryViewModel>(context, listen: false)
        .selectCategoryByOtherType(),
    builder: (context, snapshot) {
      switch (snapshot.connectionState) {
        case ConnectionState.waiting:
          return const Center(child: CircularProgressIndicator());
        case ConnectionState.done:
          return Consumer<SelectCategoryViewModel>(
            child: const Center(child: Text('No Data')),
            builder: (context, provider, child) => provider.otherData.isEmpty
                ? child!
                : ListView.separated(
                    shrinkWrap: true,
                    physics: const NeverScrollableScrollPhysics(),
                    itemCount: provider.otherData.length,
                    separatorBuilder: (_, __) => const SizedBox(height: 16),
                    itemBuilder: (_, index) {
                      final category = provider.otherData[index];
                      return SelectCategoryCard(
                        id: category.id,
                        coloredIcon: category.categoryIcon,
                        title: category.categoryName,
                        isOptionDotsVisitable: provider.getEditOptionData,
                      );
                    },
                  ),
          );
        default:
          return Container();
      }
    },
  );
}

额外优化

可以在ViewModel中添加初始化方法,一次性加载所有分类数据,避免三个FutureBuilder各自触发查询:

// ViewModel中添加初始化方法
Future<void> initData() async {
  await Future.wait([
    selectCategoryByExpenseType(),
    selectCategoryByIncomeType(),
    selectCategoryByOtherType(),
  ]);
}

// 在页面的initState中调用初始化
@override
void initState() {
  super.initState();
  Provider.of<SelectCategoryViewModel>(context, listen: false).initData();
}

这样页面可以简化为直接用Consumer监听数据变化,无需多个FutureBuilder。


内容的提问来源于stack exchange,提问作者md-siam

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 17:20:42