Search code examples
flutterbloc

Bloc not emmiting events (working minimal example)


My CombosBloc does not receive the CategoryChangedEvent.

  1. The Bloc is create on main with BlocProvider(create: (_) => CombosBloc(), lazy: false)

  2. On the TaskPage, which is a few clicks away from the Home, I construct another page, let's called EditPage, inherited from TaskPage, by clicking in a button. In the initState of the EditPage, I try to emit an event like this: context.read<CombosBloc>().add(CategoryChangedEvent(newCategory: widget.task!['category']));

  3. Passed event 2, when the initState ends, starts the building process for the widgets of the EditPage. Inside those widgets, there is a DropdownButtonHideUnderline, which in it's own initState, tries to read a value from the CombosBloc like this: selected = context.read<CombosBloc>().state.category; The value read is empty, which causes a crash. I have checked that the value in 'widget.task!['category']' is valid in step 2.

  4. Weirdly enough, the behavior's above happens when I debug the app. When I ran without debugging, only then, I see this change on the console from the Observer I created: CombosBloc Change { currentState: CombosInitial(, Todas, Mais recentes, false), nextState: CombosState(Estudos, Todas, Mais recentes, false) }. This gets printed only after all 9885 StackTraces messages in the console.

My minimal working example:

import 'package:flutter/material.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';

void main() {
  Bloc.observer = const Observer();
  runApp(
    MultiBlocProvider(providers: [
      BlocProvider(create: (_) => CombosBloc(), lazy: false),
    ], child: EntryPoint()),
  );
}

class Observer extends BlocObserver {
  const Observer();
  @override
  void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
    super.onChange(bloc, change);
    // ignore: avoid_print
    print('${bloc.runtimeType} $change');
  }
}

class AppRouter {
  final GoRouter router = GoRouter(
    initialLocation: '/',
    routes: [
      GoRoute(
          name: 'home',
          path: '/',
          builder: (context, state) => const FirstPage()),
    ],
  );
}

class EntryPoint extends StatelessWidget {
  EntryPoint({super.key});

  final _appRouter = AppRouter();

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      debugShowCheckedModeBanner: false,
      title: 'Testing',
      routerConfig: _appRouter.router,
    );
  }
}

class FirstPage extends StatefulWidget {
  const FirstPage({super.key});

  @override
  State<FirstPage> createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Testing')),
      body: Center(
        child: TextButton(
          child: const Text('Click me!'),
          onPressed: () async {
            await Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => const SecondPage(),
              ),
            );
          },
        ),
      ),
    );
  }
}

class SecondPage extends StatefulWidget {
  const SecondPage({super.key});

  @override
  State<SecondPage> createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
  @override
  void initState() {
    context
        .read<CombosBloc>()
        .add(const CategoryChangedEvent(newCategory: 'testing'));
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Second Page')),
      body: const Center(
        child: Combo(),
      ),
    );
  }
}

class Combo extends StatefulWidget {
  const Combo({super.key});

  @override
  State<Combo> createState() => _ComboState();
}

class _ComboState extends State<Combo> {
  final List<String> values = ['1', '2', '3'];
  late String selected;

  @override
  void initState() {
    selected = context.read<CombosBloc>().state.category;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(16.0),
        border: Border.all(),
      ),
      child: DropdownButtonHideUnderline(
        child: DropdownButton<String>(
          borderRadius: BorderRadius.circular(32.0),
          value: selected,
          iconSize: 28,
          isExpanded: true,
          icon: const Icon(Icons.arrow_downward_sharp),
          elevation: 16,
          onChanged: (String? newValue) {},
          items: values.map<DropdownMenuItem<String>>((String value) {
            return DropdownMenuItem<String>(
              value: value,
              child: Center(
                  child: Text(
                value,
                overflow: TextOverflow.ellipsis,
              )),
            );
          }).toList(),
        ),
      ),
    );
  }
}

/// ---------- BLOC ------------ ///

class CombosBloc extends Bloc<CombosEvent, CombosState> {
  CombosBloc() : super(const CombosInitial()) {
    on<CategoryChangedEvent>((event, emit) {
      emit(state.copyWith(category: event.newCategory));
    });
  }
}

sealed class CombosEvent extends Equatable {
  const CombosEvent();

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

final class CategoryChangedEvent extends CombosEvent {
  const CategoryChangedEvent({required this.newCategory});
  final String newCategory;
}

final class CombosState extends Equatable {
  const CombosState({
    required this.category,
  });

  final String category;

  CombosState copyWith({
    String? category,
  }) {
    return CombosState(
      category: category ?? this.category,
    );
  }

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

final class CombosInitial extends CombosState {
  const CombosInitial() : super(category: '');
}

Solution

  • It seems the add(SomeEvent) of the Bloc is not synchronous, so, by the time my DropdownButton was being created, the value hasn't been updated yet. I solved by changing into Cubit and managing the value using getters and setters. That solved my issue.