Flutter中Bloc实现ToggleButton无响应问题求助
我用Flutter的Bloc模式实现ToggleButton,点击按钮更新索引并通过Bloc发射状态切换bool值,但UI完全无变化。排查后推测状态更新未生效,试过BlocListener也无效,可能是状态发射相关问题。以下是相关代码:
Bloc代码
import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/cupertino.dart'; part 'calculate_event.dart'; part 'calculate_state.dart'; class CalculateBloc extends Bloc<CalculateEvent, CalculateState> { ///Constructor CalculateBloc() : super(CalculateInitial( selection: List.filled(3, false), totalAmount: '', textEditingController: TextEditingController())) { ///On-Method to emit the State on<CalculateEvent>((event, emit) { ///UpdateSelectionEvent if (event is UpdateSelectionEvent) { ///emit the Index of the button-press final updateSelect = List.of(state.selection); for (int i = 0; i < updateSelect.length; i++) { updateSelect[i] = event.selectIndex == i; } emit(UpdateSelectionState( selection: updateSelect, totalAmount: state.TotalAmount, tip: state.tip, textEditingController: state.textEditingController)); } ///CalculatePercentageEvent if (event is CalculatePercentageEvent) { final controller = state.textEditingController; final totalAmount = double.parse(controller.text); final selectedIndex = state.selection.indexWhere((element) => element); final tipPercentage = [0.1, 0.15, 0.2][selectedIndex]; final tipTotal = (totalAmount * tipPercentage).toStringAsFixed(2); emit(CalculatePercentageState( selection: state.selection, totalAmount: totalAmount.toString(), tip: tipTotal, textEditingController: state.textEditingController)); } }); } }
State类代码
part of 'calculate_bloc.dart'; @immutable abstract class CalculateState extends Equatable { final List<bool> selection; final String? tip; final String TotalAmount; final TextEditingController textEditingController; CalculateState( {this.tip = '', required this.TotalAmount, required this.selection, required this.textEditingController}); @override List<Object?> get props => [selection, tip, TotalAmount]; } class CalculateInitial extends CalculateState { CalculateInitial( {required List<bool> selection, String? tip = '', required String totalAmount, required TextEditingController textEditingController}) : super( selection: selection, TotalAmount: totalAmount, textEditingController: textEditingController); } class CalculatePercentageState extends CalculateState { CalculatePercentageState( {required List<bool> selection, String? tip = '', required String totalAmount, required TextEditingController textEditingController}) : super( selection: selection, TotalAmount: totalAmount, tip: tip, textEditingController: textEditingController); } class UpdateSelectionState extends CalculateState { UpdateSelectionState( {required List<bool> selection, String? tip = '', required String totalAmount, required TextEditingController textEditingController}) : super( selection: selection, TotalAmount: totalAmount, tip: tip, textEditingController: textEditingController); }
Event类代码
part of 'calculate_bloc.dart'; @immutable abstract class CalculateEvent {} class CalculatePercentageEvent extends CalculateEvent {} class UpdateSelectionEvent extends CalculateEvent { final int selectIndex; UpdateSelectionEvent(this.selectIndex); }
UI代码
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_day_1/bloc/calculate_bloc.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => CalculateBloc(), child: Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ BlocBuilder<CalculateBloc, CalculateState>( bloc: BlocProvider.of<CalculateBloc>(context), builder: (context, state) { if (state.tip != null) { return Column( children: [ Padding( padding: const EdgeInsets.all(20), child: Text(state.tip ?? '', style: const TextStyle(fontSize: 30)), ), const Text('Total Amount'), SizedBox( width: 80, child: TextField( controller: state.textEditingController, keyboardType: const TextInputType.numberWithOptions(), textAlign: TextAlign.center, decoration: const InputDecoration(hintText: '\$100.00'), ), ), Padding( padding: const EdgeInsets.all(20), child: ToggleButtons( onPressed: (index) => BlocProvider.of<CalculateBloc>(context) .add(UpdateSelectionEvent(index)), isSelected: state.selection, children: const [ Text('10%'), Text('15%'), Text('20%'), ], ), ), ], ); } else { return Container( child: const Text('flase'), ); } }), ElevatedButton( onPressed: () { BlocProvider.of<CalculateBloc>(context) .add(CalculatePercentageEvent()); }, child: const Text('Calculate Amount'), ), ], ), ), ), ); } }
问题原因及修复方案
核心问题
- Equatable状态判断失效:抽象类
CalculateState的props未包含textEditingController,但每次发射状态时传递的是同一个controller实例,Equatable会认为状态未变化,导致BlocBuilder不触发UI重建。同时,TextEditingController是可变对象,不适合存放在不可变的Bloc状态中。 - BlocBuilder重复获取实例:手动传递
bloc: BlocProvider.of<CalculateBloc>(context)可能导致监听异常,BlocBuilder会自动从上下文获取实例,无需手动传递。 - 缺少输入合法性判断:计算时未处理输入为空或未选中ToggleButton的情况,容易引发崩溃。
具体修复步骤
1. 重构CalculateState,移除可变对象
将TextEditingController从状态中移除,放在UI层管理:
part of 'calculate_bloc.dart'; @immutable abstract class CalculateState extends Equatable { final List<bool> selection; final String? tip; final String totalAmount; const CalculateState({ this.tip = '', required this.totalAmount, required this.selection, }); @override List<Object?> get props => [selection, tip, totalAmount]; } class CalculateInitial extends CalculateState { const CalculateInitial({ required List<bool> selection, String? tip = '', required String totalAmount, }) : super( selection: selection, totalAmount: totalAmount, tip: tip, ); } class CalculatePercentageState extends CalculateState { const CalculatePercentageState({ required List<bool> selection, String? tip = '', required String totalAmount, }) : super( selection: selection, totalAmount: totalAmount, tip: tip, ); } class UpdateSelectionState extends CalculateState { const UpdateSelectionState({ required List<bool> selection, String? tip = '', required String totalAmount, }) : super( selection: selection, totalAmount: totalAmount, tip: tip, ); }
2. 修改Bloc代码,移除controller相关逻辑
import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; part 'calculate_event.dart'; part 'calculate_state.dart'; class CalculateBloc extends Bloc<CalculateEvent, CalculateState> { CalculateBloc() : super(const CalculateInitial( selection: [false, false, false], totalAmount: '')) { on<CalculateEvent>((event, emit) { if (event is UpdateSelectionEvent) { final updateSelect = List.of(state.selection); for (int i = 0; i < updateSelect.length; i++) { updateSelect[i] = event.selectIndex == i; } emit(UpdateSelectionState( selection: updateSelect, totalAmount: state.totalAmount, tip: state.tip, )); } if (event is CalculatePercentageEvent) { if (state.totalAmount.isEmpty) return; final totalAmount = double.parse(state.totalAmount); final selectedIndex = state.selection.indexWhere((element) => element); if (selectedIndex == -1) return; // 处理未选中按钮的情况 final tipPercentage = [0.1, 0.15, 0.2][selectedIndex]; final tipTotal = (totalAmount * tipPercentage).toStringAsFixed(2); emit(CalculatePercentageState( selection: state.selection, totalAmount: state.totalAmount, tip: tipTotal, )); } // 添加输入框值同步事件处理 if (event is UpdateTotalAmountEvent) { emit(UpdateSelectionState( selection: state.selection, totalAmount: event.totalAmount, tip: state.tip, )); } }); } }
3. 更新Event类,添加输入同步事件
part of 'calculate_bloc.dart'; @immutable abstract class CalculateEvent {} class CalculatePercentageEvent extends CalculateEvent {} class UpdateSelectionEvent extends CalculateEvent { final int selectIndex; UpdateSelectionEvent(this.selectIndex); } // 添加输入框值同步事件 class UpdateTotalAmountEvent extends CalculateEvent { final String totalAmount; UpdateTotalAmountEvent(this.totalAmount); }
4. 重构UI代码,管理controller并优化BlocBuilder
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_day_1/bloc/calculate_bloc.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { final _textEditingController = TextEditingController(text: '100.00'); @override void dispose() { _textEditingController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BlocProvider( create: (context) => CalculateBloc(), child: Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ BlocBuilder<CalculateBloc, CalculateState>( builder: (context, state) { return Column( children: [ Padding( padding: const EdgeInsets.all(20), child: Text(state.tip ?? '', style: const TextStyle(fontSize: 30)), ), const Text('Total Amount'), SizedBox( width: 80, child: TextField( controller: _textEditingController, keyboardType: const TextInputType.numberWithOptions(), textAlign: TextAlign.center, decoration: const InputDecoration(hintText: '\$100.00'), onChanged: (value) { // 实时同步输入值到Bloc context.read<CalculateBloc>().add(UpdateTotalAmountEvent(value)); }, ), ), Padding( padding: const EdgeInsets.all(20), child: ToggleButtons( onPressed: (index) => context.read<CalculateBloc>().add(UpdateSelectionEvent(index)), isSelected: state.selection, children: const [ Text('10%'), Text('15%'), Text('20%'), ], ), ), ], ); }, ), ElevatedButton( onPressed: () { context.read<CalculateBloc>().add(CalculatePercentageEvent()); }, child: const Text('Calculate Amount'), ), ], ), ), ), ); } }
内容的提问来源于stack exchange,提问作者RefRies
相关产品推荐
相关产品推荐

