stream_listener_widget 1.0.0
stream_listener_widget: ^1.0.0 copied to clipboard
A simple widget that listen streams without rebuilding its child (ex. to show a dialog)
stream_listener_widget #
A simple widget that listen streams without rebuilding its child (ex. to show a dialog)
Keep it simple #
- A simple logic class (ViewModel, Controller, etc) should not make use of BuildContext
- Actually it should not trigger any popup or navigation because it's View's responsibility
The StreamListener widget is as simple as Flutter's StreamBuilder widget.
- It allows you to react to the given streams' events without rebuilding any widget
- And it will clean up memory for you by cancelling all the given streams' subscriptions
Here's what you should avoid to do (handling navigation, popup, etc in your logic class):
class MyController {
BuildContext context;
Future<void> submitForm(String email, String password) {
try {
await authenticationApi.login(email, password);
Navigator.of(context).pushNamed('/homePage');
} catch(e) {
showDialog(
context: context,
barrierDismissible: true,
builder: (context) => Dialog(child: Text('An unknown error occured')),
);
}
}
}
class MyView extends StatefulWidget {
const MyView({super.key});
@override
State<MyView> createState() => _MyViewState();
}
class _MyViewState extends State<MyView> {
final logic = MyController();
@override
Widget build(BuildContext context) {
logic.context = context; // That's really ugly
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => logic.submitForm('my_email', 'my_password'),
child: const Text('Submit'),
),
);
}
}
But wait ?! The example above is shorter !
- Yes ! But we haven't separated View logic & Business logic
- Our controller is doing everything while the View is doing nothing
- Responsibilities and dependencies are mixed up
- Your controller has been designed for this specific View
- BuildContext is used after a Future/async gap which is forbidden because it can be unstable
Instead your logic could just trigger some events handled by the View:
- Our view handle View's logic
- Our logic class handle Business logic
- Responsibilities & dependencies are well separated
- Our controller has been Domain Driven Designed and can be used elsewhere :)
- By defining a Type to logic's events the Business logic is clear
- The code is stable and testable
/// Here's some Type classes that defines possible domain/logic's events
sealed class MyEvent {}
class LoginSuccess extends MyEvent {}
class LoginError extends MyEvent {}
class MyController {
final controller = StreamController<MyEvent>.broadcast();
Future<void> submitForm(String email, String password) {
try {
await authenticationApi.login(email, password);
controller.add(LoginSuccess());
} catch(e) {
controller.add(LoginError());
}
}
void dispose() => controller.close();
}
class MyView extends StatefulWidget {
/// We let the possibility to override the default controller for testability
final MyController? controller;
const MyView({super.key, this.controller});
@override
State<MyView> createState() => _MyViewState();
}
class _MyViewState extends State<MyView> {
late final logic = widget.controller ?? MyController();
void _onError(LoginError e) {
showDialog(
context: context,
barrierDismissible: true,
builder: (context) => Dialog(child: Text('An unknown error occured')),
);
}
void _onLoginSuccess(LoginSuccess e) {
Navigator.of(context).pushNamed('/homePage');
}
@override
void dispose() {
logic.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return StreamListener(
listeners: [
(context) => logic.controller.stream.whereType<LoginSuccess>().listen(_onMaxReached),
(context) => logic.controller.stream.whereType<LoginError>().listen(_onError),
],
child: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => logic.submitForm('my_email', 'my_password'),
child: const Text('Submit'),
),
),
);
}
}