Resource
Resources
are special Signal
s designed specifically to handle async loading.
Their purpose is wrap async values in a way that makes them easy to interact by handling the common states of an async value: data, error and loading.
Let’s create a Resource:
// Using http as a clientimport 'package:http/http.dart' as http;
// The fetcherFuture<String> fetchUser() async { final response = await http.get( Uri.parse('https://jsonplaceholder.typicode.com/users/1'), headers: {'Accept': 'application/json'}, ); return response.body;}
final user = Resource(fetchUser);
// and use it in a widget:SignalBuilder( builder: (context, child) { return user.state.on( ready: (data) => Text(data), error: (error, stackTrace) => Text('Error: $error'), loading: () => const Text('Loading...'), ); },),
A Resource can also be driven by a Stream
.
final stream = Stream.periodic( const Duration(seconds: 1), (count) => 'Tick: $count',);
final resource = Resource.stream(() => stream);
// the widget usage is the same as above, no changes needed
You may ask yourself: why not just use a Future
or a Stream
directly?
FutureBuilder is overcomplicated
In my experience, FutureBuilder
is overcomplicated and hard to use correctly.
Even experienced Flutter developers cause side effects, like this:
FutureBuilder<String>( future: downloadData(), // function where you call your api ...),
which causes the downloadData
function to be called multiple times, because the build
method is called multiple times.
Additionally AsyncSnapshot
is a complex class, why do you need to deal with ConnectionState
? Things are overcomplicated.
Stream
Ever wanted to get the latest value of a Stream
synchronously?
You don’t have a way, because Stream
is a pipeline, if you start listening to it too later, you miss the previous values.
That’s why people starts using rxdart
with the BehaviorSubject
, which is a Stream
that also holds the latest value.
But Resource
will hold the current and also the previous states.
Resources can switch the async source at runtime
This is a very powerful feature of Resource
.
You can create a Resource
that depends on another Signal
, and when that Signal
changes, the Resource
will automatically fetch the new data.
For example, you can get the user data based on a user id Signal
:
final userId = Signal<String?>(null);late final userData = Resource( () { return switch (userId.value) { // If userId is null, return null null => Future.value(null), // Otherwise fetch the user data final id => getUserData(id), }; }, // Everytime the userId changes, the resource will be triggered to fetch new data source: userId,);
This let’s you achieve complex async flows with very little code.
The same works for Resource.stream
:
final userId = Signal<String?>(null);late final userData = Resource.stream( () { return switch (userId.value) { // If userId is null, return an empty stream null => Stream.value(null), // Otherwise fetch the user data stream final id => getUserDataStream(id), }; }, // Everytime the userId changes, the resource will be triggered to fetch new data source: userId,);
If your Resource
depends on multiple Signal
s, you can use a Computed
as source:
final userId = Signal<String?>(null);final authToken = Signal<String?>(null);final source = Computed(() => (userId.value, authToken.value));
Resource.debounceDelay
You can debounce a Resource
by using the debounceDelay
parameter:
Resource( ..., debounceDelay: const Duration(seconds: 1),)
This will wait for 1 second after the last change of the source
before triggering the fetcher.
Use it when you want to avoid multiple calls in a short time, for example when the source
is driven by a text field.
Resource.refresh()
You can manually refresh a Resource
by calling the refresh
method.
Resource.useRefreshing
By default, when the Resource refreshes, the state will NOT transition to loading
.
Instead, it will set isRefreshing
to true, and the state will remain in the current state (either ready
or error
).
This allows you to keep showing the current data or error while the new data is being fetched.
SignalBuilder(builder: (context, child) { final userState = user.state; return userState.on( ready: (data) { return Column( mainAxisSize: MainAxisSize.min, children: [ Text(data), userState.isRefreshing ? const CircularProgressIndicator() : ElevatedButton( onPressed: user.refresh, child: const Text('Refresh'), ), ], ); }, error: (e, _) { return Column( mainAxisSize: MainAxisSize.min, children: [ Text(e.toString()), userState.isRefreshing ? const CircularProgressIndicator() : ElevatedButton( onPressed: user.refresh, child: const Text('Refresh'), ), ], ); }, loading: () => CircularProgressIndicator(), );}),
By setting useRefreshing
to false, the state will always transition to loading
when refreshing.
You can also update it globally:
void main() { SolidartConfig.useRefreshing = false; runApp(const MyApp());}