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

Flutter中Bloc实现ToggleButton无响应问题求助

Flutter Bloc实现ToggleButton无UI更新问题

我用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'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

问题原因及修复方案

核心问题

  1. Equatable状态判断失效:抽象类CalculateState的props未包含textEditingController,但每次发射状态时传递的是同一个controller实例,Equatable会认为状态未变化,导致BlocBuilder不触发UI重建。同时,TextEditingController是可变对象,不适合存放在不可变的Bloc状态中。
  2. BlocBuilder重复获取实例:手动传递bloc: BlocProvider.of<CalculateBloc>(context)可能导致监听异常,BlocBuilder会自动从上下文获取实例,无需手动传递。
  3. 缺少输入合法性判断:计算时未处理输入为空或未选中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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 00:20:30