Skip to content

Resource

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 by handling the common states of an async value: data, error and loading.

Let’s create a Resource:

// Using http as a client
import 'package:http/http.dart' as http;
// The fetcher
Future<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 Signals, 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());
}