flutter_solidart 1.0.0-dev2 flutter_solidart: ^1.0.0-dev2 copied to clipboard
A simple State Management solution for Flutter applications inspired by SolidJS
A simple state-management library inspired by SolidJS. #
The objectives of this project are:
- Being simple and easy to learn
- Do not go against the framework (e.g. Flutter) with weird workarounds.
- Do not have a single global state, but put multiple states only in the most appropriate places
Learning #
For a comprehensive and updated documentation go to The Official Documentation
There are 4 main concepts you should be aware:
Signals #
Signals are the cornerstone of reactivity in solidart. They contain values that change over time; when you change a signal's value, it automatically updates anything that uses it.
To create a signal, you have to use the createSignal
method:
final counter = createSignal(0);
The argument passed to the create call is the initial value, and the return value is the signal.
// Retrieve the current counter value
print(counter.value); // prints 0
// equal to
print(counter());
// Change the counter value
counter.value++; // Increments by 1
// or
counter.value = 2; // Sets the value to 2
// or
counter.set(3);
// or
counter.update((value) => value * 2); // Update the value based on the current value
If you're using flutter_solidart
you can use the SignalBuilder
widget to automatically react to the signal value, for example:
SignalBuilder(
signal: counter,
builder: (_, value, __) {
return Text('$value');
},
)
Effects #
Signals are trackable values, but they are only one half of the equation. To complement those are observers that can be updated by those trackable values. An effect is one such observer; it runs a side effect that depends on signals.
An effect can be created by using createEffect
.
The effect automatically subscribes to any signal and reruns when any of them change.
So let's create an Effect that reruns whenever counter
changes:
final disposeFn = createEffect((_) {
print("The count is now ${counter.value}");
});
Resources #
Resources are special Signals designed specifically to handle Async loading. Their purpose is wrap async values in a way that makes them easy to interact with.
Resources can be driven by a source
signal that provides the query to an async data fetcher
function that returns a Future
.
The contents of the fetcher
function can be anything. You can hit typical REST endpoints or GraphQL or anything that generates a future. Resources are not opinionated on the means of loading the data, only that they are driven by futures.
Let's create a Resource:
// The source
final userId = createSignal(1);
// The fetcher
Future<String> fetchUser() async {
final response = await http.get(
Uri.parse('https://swapi.dev/api/people/${userId.value}/'),
);
return response.body;
}
// The resource
final user = createResource(fetcher: fetchUser, source: userId);
A Resource can also be driven from a [stream] instead of a Future.
In this case you just need to pass the stream
field to the createResource
method.
The [source] field is ignored for the [stream] and used only for a [fetcher].
Using ResourceBuilder
you can react to the state of the resource:
ResourceBuilder(
resource: user,
builder: (_, resource) {
return resource.on(
// the call was successful
ready: (data, refreshing) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(data),
subtitle: Text('refreshing: $refreshing'),
),
ElevatedButton(
// you can refetch if you want to update the data
onPressed: user.refetch,
child: const Text('Refresh'),
),
],
);
},
// the call failed.
error: (e, _) => Text(e.toString()),
// the call is loading.
loading: () {
return const RepaintBoundary(
child: CircularProgressIndicator(),
);
},
);
},
)
The on
method forces you to handle all the states of a Resource (ready, error and loading).
The are also other convenience methods to handle only specific states.
Solid #
The Flutter framework works like a Tree. There are ancestors and there are descendants.
You may incur the need to pass a Signal deep into the tree, this is discouraged. You should never pass a signal as a parameter.
To avoid this there's the Solid widget.
With this widget you can pass a signal down the tree to anyone who needs it.
You will have already seen Theme.of(context)
or MediaQuery.of(context)
, the procedure is practically the same.
Let's see an example to grasp the concept.
You're going to see how to build a toggle theme feature using Solid
, this example is present also here https://github.com/nank1ro/solidart/tree/main/examples/toggle_theme
/// The identifiers used for [Solid] signals.
enum SignalId { // [1]
themeMode,
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Provide the theme mode signal to descendats
return Solid( // [2]
signals: {
// the id of the signal and the signal associated.
SignalId.themeMode: () => createSignal<ThemeMode>(ThemeMode.light),
},
child:
// using a builder here because the `context` must be a descendant of [Solid]
Builder(
builder: (context) {
// observe the theme mode value this will rebuild every time the themeMode signal changes.
final themeMode = context.observe<ThemeMode>(SignalId.themeMode); // [3]
return MaterialApp(
title: 'Toggle theme',
themeMode: themeMode,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
home: const MyHomePage(),
);
},
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
// retrieve the theme mode signal
final themeMode = context.get<Signal<ThemeMode>>(SignalId.themeMode); // [4]
return Scaffold(
appBar: AppBar(
title: const Text('Toggle theme'),
),
body: Center(
child:
// Listen to the theme mode signal rebuilding only the IconButton
SignalBuilder( // [5]
signal: themeMode,
builder: (_, mode, __) {
return IconButton(
onPressed: () { // [6]
// toggle the theme mode
if (themeMode.value == ThemeMode.light) {
themeMode.value = ThemeMode.dark;
} else {
themeMode.value = ThemeMode.light;
}
},
icon: Icon(
mode == ThemeMode.light ? Icons.dark_mode : Icons.light_mode,
),
);
},
),
),
);
}
}
In this example many things occured, first at [1]
we've used an Enum to store all the [SignalId]s.
You may use a String
, an int
or wethever you want. Just be sure to use the same id to retrieve the signal.
Then at [2]
we've used the Solid
widget to provide the themeMode
signal to descendants.
The Solid
widgets takes a Map of signals
:
- The key of the Map is the signal id, in this case
SignalId.themeMode
. - The value of the Map is a function that returns a
SignalBase
. You may create a signal or a derived signal. The value is a Function because the signal is created lazily only when used for the first time, if you never access the signal it never gets created.
At [3]
we observe
the value of a signal. The observe
method listen to the signal value and rebuilds the widget when the value changes. It takes an id
that is the signal identifier that you want to use. This method must be called only inside the build
method.
At [4]
we get
the signal with the given id
. This doesn't listen to signal value. You may use this method inside the initState
and build
methods.
At [5]
using the SignalBuilder
widget we rebuild the IconButton
every time the signal value changes.
And finally at [6]
we update the signal value.
It is mandatory to pass the type of signal value otherwise you're going to encounter an error, for example:
createSignal<ThemeMode>
andcontext.observe<ThemeMode>
where ThemeMode is the type of the signal value.context.get<Signal<ThemeMode>>
whereSignal<ThemeMode>
is the type of signal with its type value.
Examples #
Sample features using flutter_solidart: #
Showcase of all flutter_solidart features #
Learn every feature of flutter_solidart
including:
createSignal
Show
widget- Derived signals with
createComputed
Effect
s with basic and advanced usagesSignalBuilder
,DualSignalBuilder
andTripleSignalBuilder
createResource
andResourceBuilder
Solid
and its fine-grained reactivity- Access signals in modals with
Solid.value