Normally when testing widgets I would like to avoid having to mock and provide a bloc, just to provide state values.
This means we end up with two widgets, one that is our actual UI Widget, and one that wraps our UI widget in a BlocBuilder and passes along the state it needs.
Since this seems to be a pretty common thing, I wondered if that could be shortened a bit.
React Redux has a connect function, that sort of achieves this. It makes more sense in a Redux function with the global store, but perhaps it could help us here.
Widget connect<B extends Bloc<E, S>, E, S>(
Widget Function(S, void Function(E)) builder) {
return BlocBuilder<B, S>(
builder: (context, state) =>
builder(state, BlocProvider.of<B>(context).add),
);
}
A simple example would be
class WorkoutScreen extends StatelessWidget {
final String workoutName;
final List<String> exerciseNames;
final Function onComplete;
WorkoutScreen({
@required this.workoutName,
@required this.exerciseNames,
@required this.onComplete,
});
}
final Widget ConnectedWorkoutScreen =
connect<WorkoutBloc, WorkoutEvent, WorkoutState>(
(state, add) => WorkoutScreen(
workoutName: state.workoutName,
exerciseNames: state.exerciseNames,
onComplete: () => add(WorkoutEvent()),
));
As an alternative, you could WorkoutBloc when you provide it to the widget.
BlocProvider(create: (_) => MockWorkoutBloc())
Then as part of your test setup, you can define mock expectations.
@chimon2000 That links my widget too closely to Bloc, I don't want that.
Makes it harder to test too.. I need to both mock a block and make it emit the state that I want to test, when I really could just directly pass it the state by splitting them up.
Especially annoying with multiple blocs, having to mock all the blocs they depend on.
@Jomik you could just split your widget into a WorkoutScreen and WorkoutView:
class WorkoutScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<WorkoutBloc, WorkoutState>(
builder: (context, state) {
return WorkoutView(
workoutName: state.workoutName,
exerciseNamed: state.exerciseNames,
onComplete: () => context.bloc<WorkoutBloc>().add(WorkoutEvent()),
);
},
);
}
}
The Container/Presenter pattern that @felangel mentions above is what I was implying. Should have been more explicit.
Yeah, that is also what I am doing now. I just feel like that is boilerplate that we could avoid with a helper.
@Jomik I'm a bit conflicted because I see your point but on the other hand, I don't feel there is that much boilerplate with the current approach and it's a lot easier to read/understand imo as opposed to the connect API from react. Thoughts?
@Jomik I'm a bit conflicted because I see your point but on the other hand, I don't feel there is that much boilerplate with the current approach and it's a lot easier to read/understand imo as opposed to the connect API from react. Thoughts?
I have the same thoughts as you here really.
The reason I feel like this could be considered, is if we often create a StatelessWidget that simply has a BlocBuilder whose builder maps the state to a child "View" Widget. Then that becomes "boilerplate" in my head.
Looking at this more, I do not like the idea of providing the add function. It makes more sense to just give the BuildContext so that the developer can get whatever they need from the context, which would also save us from passing in the Event type generic.
final Widget ConnectedWorkoutScreen = connect<WorkoutBloc, WorkoutState>(
(state, context) => WorkoutScreen(
workoutName: state.workoutName,
exerciseNames: state.exerciseNames,
onComplete: () {
context.bloc<WorkoutBloc>().add(WorkoutEvent.complete());
},
),
);
class ConnectedWorkoutScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<WorkoutBloc, WorkoutState>(
builder: (context, state) => WorkoutScreen(
workoutName: state.workoutName,
exerciseNames: state.exerciseNames,
onComplete: () {
context.bloc<WorkoutBloc>().add(WorkoutEvent.start());
},
),
);
}
}
Writing this, I realise that using my connect function also makes you unable to use that neat static Route route(..) function that you opened my eyes to recently, so that is definitely a negative - for screens that you can navigate to.
I suppose many widgets do not need that route method though.
I am fine with us closing this. If I end up writing "the boilerplate" a lot, I may test out this idea in my own app, and the we can always revisit this issue.
@Jomik I don鈥檛 view that as boilerplate because that code is what determines what UI to render based on the bloc state and is super important.
Sounds good will close this for now but feel free to reopen if you have additional concerns and thanks for bringing the topic up! 馃槃
Most helpful comment
The Container/Presenter pattern that @felangel mentions above is what I was implying. Should have been more explicit.