Flutter Bloc, UI not updating when state is emmited

Viewed 116

I have an app where I'm emitting budgetLoaded() in several events. I have noticed that if I don't emit a different state with my budgetLoading even the app doesn't seem to respond to the change.

import 'package:bloc/bloc.dart';
import 'package:budget_app/model/budget.dart';
import 'package:budget_app/model/budget_repository.dart';
import 'package:equatable/equatable.dart';

part 'budget_event.dart';
part 'budget_state.dart';

class BudgetBloc extends Bloc<BudgetEvent, BudgetState> {
  final BudgetRepository budgetRepository;

  BudgetBloc({required this.budgetRepository}) : super(BudgetInitial()) {
    on<AppStarted>((event, emit) async {
      emit(BudgetLoading());
      if (await budgetRepository.hasBudget()) {
        emit(BudgetLoaded(budget: budgetRepository.budget));
      } else {
        emit(BudgetLoaded(budget: budgetRepository.budget));
      }
    });

    on<Withdraw>((event, emit) {
      budgetRepository.budget.withdraw(event.amount);
      emit(BudgetLoaded(budget: budgetRepository.budget));
    });

    on<Deposit>((event, emit) {
      emit(BudgetLoading());
      budgetRepository.budget.deposit(event.amount);
      emit(new BudgetLoaded(budget: budgetRepository.budget));
    });
  }
}

This is an issue with the deposit event. This code works.

import 'package:bloc/bloc.dart';
import 'package:budget_app/model/budget.dart';
import 'package:budget_app/model/budget_repository.dart';
import 'package:equatable/equatable.dart';

part 'budget_event.dart';
part 'budget_state.dart';

class BudgetBloc extends Bloc<BudgetEvent, BudgetState> {
  final BudgetRepository budgetRepository;

  BudgetBloc({required this.budgetRepository}) : super(BudgetInitial()) {
    on<AppStarted>((event, emit) async {
      emit(BudgetLoading());
      if (await budgetRepository.hasBudget()) {
        emit(BudgetLoaded(budget: budgetRepository.budget));
      } else {
        emit(BudgetLoaded(budget: budgetRepository.budget));
      }
    });

    on<Withdraw>((event, emit) {
      budgetRepository.budget.withdraw(event.amount);
      emit(BudgetLoaded(budget: budgetRepository.budget));
    });

    on<Deposit>((event, emit) {
      budgetRepository.budget.deposit(event.amount);
      emit(new BudgetLoaded(budget: budgetRepository.budget));
    });
  }
}

This code does not work the state that is emitted is correct, I have unit tests to test this and I've done debugging.

This is the main page, the BlocProvider is in the Main file above the Material App so it should be global.


import 'package:budget_app/bloc/budget_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

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

  @override
  State<WalletScreen> createState() => _WalletScreenState();
}

class _WalletScreenState extends State<WalletScreen> {
  @override
  Widget build(BuildContext context) {
    var theme = Theme.of(context);
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () => onPressed(context),
        child: Icon(Icons.add),
      ),
      body: BlocBuilder<BudgetBloc, BudgetState>(
        bloc: BlocProvider.of<BudgetBloc>(context),
        builder: (context, state) {
          if (state is BudgetLoaded) {
            var budget = state.budget;
            return SafeArea(
              child: Center(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: [
                    Padding(
                      padding: const EdgeInsets.all(10.0),
                      child: ClipRRect(
                        borderRadius: BorderRadius.circular(8),
                        child: Container(
                          child: Center(
                            child: Column(
                              children: [
                                Text(
                                  'Current Balance:',
                                  style: TextStyle(
                                      color: Colors.white, fontSize: 25),
                                ),
                                Text(
                                  '${budget.numberFormatted}',
                                  style: TextStyle(
                                      color: Colors.green, fontSize: 50),
                                ),
                              ],
                            ),
                          ),
                          color: Colors.blueGrey,
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            );
          } else {
            return CircularProgressIndicator();
          }
        },
      ),
    );
  }

  onPressed(BuildContext context) {
    var budgetBloc = BlocProvider.of<BudgetBloc>(context);
    budgetBloc.add(Deposit(100.00));
  }
}

It's not a huge issue I just want to understand why it doesn't work.

EVENT

part of 'budget_bloc.dart';

abstract class BudgetEvent extends Equatable {
  const BudgetEvent();

  @override
  List<Object> get props => [];
}

class AppStarted extends BudgetEvent {}

class Withdraw extends BudgetEvent {
  final double amount;

  const Withdraw(this.amount);
}

class Deposit extends BudgetEvent {
  final double amount;
  const Deposit(this.amount);
}

STATE

part of 'budget_bloc.dart';

abstract class BudgetState extends Equatable {
  const BudgetState();

  @override
  List<Object> get props => [];
}

class BudgetInitial extends BudgetState {}

class BudgetLoading extends BudgetState {}

class BudgetLoaded extends BudgetState {
  final Budget budget;

  BudgetLoaded({required this.budget});
}

1 Answers

You can see in the source of emit that it won't publish a state if it is equal to the last one:

void emit(State state) {
  try {
    if (isClosed) {
      throw StateError('Cannot emit new states after calling close');
    }
    if (state == _state && _emitted) return;
    onChange(Change<State>(currentState: this.state, nextState: state));
    _state = state;
    _stateController.add(_state);
    _emitted = true;
  } catch (error, stackTrace) {
    onError(error, stackTrace);
    rethrow;
  }
}

By default, == returns true if two objects are the same instance. This could lead to redundant state change notifications, so we usually override == and hashCode, to explicitly tell what counts as equal.

In your example Equatable overrides == and hashCode for you so you don't have to write boilerplate code. The problem is, your props list is empty, so every instance of the same class will be equal to each other. This is why you don't see a state change after the first one.

You should override the props in BudgetLoaded, so Equatable knows what should be compared:

class BudgetLoaded extends BudgetState {
  final Budget budget;

  BudgetLoaded({required this.budget});

  @override
  List<Object> get props => [budget];
}

And don't forget to update the Budget class too so that == and hashCode say they are equal, when you think they should be equal.

Related