My CombosBloc does not receive the CategoryChangedEvent.
The Bloc is create on main with BlocProvider(create: (_) => CombosBloc(), lazy: false)
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']));
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.
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: '');
}
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.