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
相关产品推荐
相关产品推荐

