I am a newbie, learning how to use bloc with freezed. i created a bottom sheet when the user click to the float action button, the bottom sheet appears. Bottom sheet contains text field and three radio groups when i click to select a radio the bottom sheet popup again like this GIF. https://drive.google.com/file/d/1iU06adGcwjEaw9z2LsmO5xS24CC6OQfT/view?usp=sharing
the bloc is:
class NoteBloc extends Bloc<NoteEvent, NoteState> {
NoteBloc() : super(NoteState.initial());
@override
Stream<NoteState> mapEventToState(
NoteEvent event,
) async* {
yield* event.map(
addNewNoteButtonEvent: (AddNewNoteButtonEvent e) async* {
yield state.copyWith(
isAddNewNoteButtonClickedState: e.isClickedEvent,
);
},
radioEvent: (RadioEvent e) async* {
yield state.copyWith(radioGroupState: e.value);
},
textInputEvent: (TextInputEvent e) async* {
yield state.copyWith(textInputState: Note(value: e.value));
},
);
}
}
the event is:
class NoteEvent with _$NoteEvent {
const factory NoteEvent.addNewNoteButtonEvent(
{required bool isClickedEvent}) = AddNewNoteButtonEvent;
const factory NoteEvent.radioEvent({required int value}) = RadioEvent;
const factory NoteEvent.textInputEvent({required String value}) =
TextInputEvent;
}
the state is:
class NoteState with _$NoteState {
const factory NoteState({
// required bool showSaveIconState,
required int radioGroupState,
required bool isAddNewNoteButtonClickedState,
required Note textInputState,
}) = _NoteState;
// initialize every state
factory NoteState.initial() => NoteState(
radioGroupState: 1,
isAddNewNoteButtonClickedState: false,
textInputState: Note(value: ''),
);
}
the home page code is:
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
return BlocConsumer<NoteBloc, NoteState>(
listener: (context, state) {
// if (state is AddNoteClickedState) {
if (state.isAddNewNoteButtonClickedState) {
// show navigator
_scaffoldKey.currentState!
.showBottomSheet((context) => const AddNewNoteBottomSheet())
.closed
.then(
(value) {
// Change state
// to tell the bloc that
// the bottom sheet is closed
context.read<NoteBloc>().add(
const NoteEvent.addNewNoteButtonEvent(
isClickedEvent: false, // !state.isAddNewNoteClickedState,
),
);
},
);
}
},
builder: (context, state) {
return DefaultTabController(
length: 3,
child: Scaffold(
key: _scaffoldKey,
// add note float action button
floatingActionButton: InkWell(
onTap: () {
// make button clicked
// to open the bottom sheet
context.read<NoteBloc>().add(
const NoteEvent.addNewNoteButtonEvent(
isClickedEvent: true, //!state.isAddNewNoteClickedState,
),
);
// print(state);
},
// for float action button
// icon shape
child: Container(
clipBehavior: Clip.antiAliasWithSaveLayer,
height: 11.h,
width: 10.h,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(1.w)),
color: const Color(accentColor),
),
// add note icon
child: Icon(
state.isAddNewNoteButtonClickedState ? Icons.save : Icons.add,
color: const Color(whiteColor),
size: 9.h,
),
),
);
},
);
}
}
AddNewNoteBottomSheet code is:
// For adding new note
class AddNewNoteBottomSheet extends StatelessWidget {
const AddNewNoteBottomSheet();
@override
Widget build(BuildContext context) {
TextEditingController _textEditingController = TextEditingController();
return Container(
// Contains all
// bottom sheet components
child: Column(
children: [
// bottom sheet body
Expanded(
child: SingleChildScrollView(
child: BlocBuilder<NoteBloc, NoteState>(
builder: (context, state) {
// print("Helllo ${state.maybeMap(orElse: (){},radioClickState: (stat)=>stat.value)}");
return Column(
children: [
// Today Radio
ListTile(
title: Text(
AppLocalizations.of(context)!.homeTodayTapTitle,
style: Theme.of(context)
.tabBarTheme
.unselectedLabelStyle,
),
contentPadding: const EdgeInsets.all(0),
autofocus: true,
leading: Radio<int>(
value: 1,
//get group value
groupValue: state.radioGroupState,
onChanged: (value) {
// tell bloc i click you
context
.read<NoteBloc>()
.add(NoteEvent.radioEvent(value: value!));
},
),
),
// Tomorrow Radio
ListTile(
contentPadding: const EdgeInsets.all(0),
title: Text(
AppLocalizations.of(context)!.homeTomorrowTapTitle,
style: Theme.of(context)
.tabBarTheme
.unselectedLabelStyle,
),
leading: Radio<int>(
value: 2,
//get group value
groupValue: state.radioGroupState,
onChanged: (value) {
// tell bloc i click you
context.read<NoteBloc>().add(
NoteEvent.radioEvent(value: value!),
);
},
),
),
// Some Day Radio
ListTile(
contentPadding: const EdgeInsets.all(0),
title: Text(
AppLocalizations.of(context)!.homeSomeDayTapTitle,
style: Theme.of(context)
.tabBarTheme
.unselectedLabelStyle,
),
leading: Radio<int>(
value: 3,
//get group value
groupValue: state.radioGroupState,
onChanged: (value) {
// tell bloc i click you
context.read<NoteBloc>().add(
NoteEvent.radioEvent(value: value!),
);
},
),
),
],
);
},
),
),
),
],
),
);
}
}
Problem definition: This problem happens because you use state.copyWith on your bloc. When you use copyWith, even if you don't update isAddNewNoteButtonClickedState
variable, your state persists that value (stays true after you set it, never changes if you don't change it) if you do not alter it. Because on copyWith method, update logic works like;
YourState copyWith(bool isAddNewNoteButtonClickedState) {
return YourState(isAddNewNoteButtonClickedState: isAddNewNoteButtonClickedState ?? this. isAddNewNoteButtonClickedState,)
}
And when you yield another state, this isAddNewNoteButtonClickedState
stays true, and your listener shows another modal since your isAddNewNoteButtonClickedState did not change.
Solution: you can solve this problem by (adding isAddNewNoteButtonClickedState: false
to other states):
class NoteBloc extends Bloc<NoteEvent, NoteState> {
NoteBloc() : super(NoteState.initial());
@override
Stream<NoteState> mapEventToState(
NoteEvent event,
) async* {
yield* event.map(
addNewNoteButtonEvent: (AddNewNoteButtonEvent e) async* {
yield state.copyWith(
isAddNewNoteButtonClickedState: e.isClickedEvent,
);
},
radioEvent: (RadioEvent e) async* {
yield state.copyWith(radioGroupState: e.value, isAddNewNoteButtonClickedState: false);
},
textInputEvent: (TextInputEvent e) async* {
yield state.copyWith(textInputState: Note(value: e.value), isAddNewNoteButtonClickedState: false);
},
);
}
}
Additional note: Since you are not transforming events in bloc, or doing something trivial, you can use cubit to simplify your state management and business logic.