A convenient Flutter architecture for happy programmers
Sprinkle is based on streams. Streams are one of the core mechanics in Dart and in Flutter. Whether you are interacting with your database (e.g. Firebase) or trying to control the order and timing of requests sent to an API, streams are the most natural solution for these scenarios - we could say that streams are idiomatic in Flutter.
Why don't we use streams everywhere for simplicity and coherence then?! Streams are considered difficult, but that reputation is slightly exaggerated. Once you understand the benefits, there is no coming back to traditional state management approaches. And Sprinkle simplifies stream complexities - it's an easy to understand and coherent solution for state management in Flutter.
Start by creating a Manager
. It's a class that manages one or many data stores. It also exposes actions as methods to change the state in these data stores.
class CounterManager extends Manager {
// 1. we create a data store (it's just a stream underneath)
// Managers are immutable, thus stores must be `final`
final counter = 0.reactive
// 2. we define some actions
void increment() => counter.value++;
void decrement() => counter.value--;
void add(int number) => counter.value += number;
}
Somewhere in the widget tree, inside the build
method we can ask for a specific Manager
by using the use
method of the BuildContext
class Counter extends StatelessWidget { // Our widgets are *always* stateless
@override
Widget build(BuildContext context) {
// 3. we include a manager with `use`
var manager = context.use<CounterManager>();
return Center(
// 4. we observe a part of widget tree
child: Observer<int>(
// 5. we listen to a specific stream from the manager
stream: manager.counter,
// 6. we rebuild once the stream changes
builder: (context, value) => Text("Counter: $value"),
),
);
}
}
Reactivity is enabled for the following types in Dart:
final value = false.reactive;
final value = 42.reactive;
final value = 'The quick brown fox jumps over the lazy dog'.reactive;
final value = <String>[].reactive
Use the Sprinkle
widget to split the responsibilities and reduce the boilerplate so that you can write this:
final supervisor = Supervisor()
.register<AManager>(() => AManager())
void main() => runApp(Sprinkle(supervisor: supervisor, child: App()));
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
...
)
}
}
instead of this:
void main() => runApp(EmailApp());
class EmailApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider(
data: Supervisor()
.register<AManager>(() => AManager())
child: MaterialApp(
...
),
);
}
}
In this example LoginManager
asks for AuthManager
. This goes through Supervisor
and the requested manager is either provided as-is or instantiated.
class LoginManager extends Manager {
void logout() {
var manager = use<AuthManager>()
manager.setAuthState(AuthState.loggedOut);
}
@override
void dispose() {}
}
Some convenience methods that follow the Pareto principle, i.e. Sprinkle makes aliases for the most common code scenarios.
To change routes, you can write
context.display(AnotherPage())
instead of
Navigator.push(
context,
MaterialPageRoute(builder: (context) => AnotherPage()),
);
To display a snackbar, you can write:
context.showSnackBar("...you text");
instead of
Scaffold.of(context).showSnackBar(
SnackBar(content: Text('Yay! A SnackBar!'))
);
to get MediaQuery Size, you can write:
context.mediaQuerySize
instead of
MediaQuery.of(context).size
to get Device orientation, you can write:
context.orientation
instead of
MediaQuery.of(context).orientation
to check if Device orientation is Landscape, you can write:
context.isLandscape
instead of
MediaQuery.of(context).orientation == Orientation.landscape
to check if Device orientation is Portrait, you can write:
context.isPortrait
instead of
MediaQuery.of(context).orientation == Orientation.portrait
Use .padding()
method on widgets directly
Column(
children: [
Text("User 1").padding(8),
Text("User 2").padding(8),
],
);
instead of wrapping them with Padding
:
Column(
children: [
Padding(
padding: EdgeInsets.all(8.0),
child: Text("User 1"),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text("User 2"),
),
],
);
Use .center()
method on widgets directly
Column(
children: [
],
).center();
instead of wrapping them with Center
:
Center(
child:Column(
children: [
],
)
);