@@ -5,52 +5,193 @@ sidebar:
55 order : 4
66---
77
8- ` 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 with handling the common states of a future __ data __ , __ error __ and __ loading __ .
8+ ` Resources ` are special ` Signal ` s designed specifically to handle __ async loading__ .
99
10- Resources can be driven by a ` source ` signal that provides the query to an async data.
11-
12- The contents of the function can be anything. You can hit typical REST endpoints or GraphQL or anything that generates a future or a stream. Resources are not opinionated on the means of loading the data, only that they are driven by futures and streams.
10+ Their purpose is wrap async values in a way that makes them easy to interact with handling the common states of an async value: __ data__ , __ error__ and __ loading__ .
1311
1412Let's create a Resource:
1513
1614``` dart
1715// Using http as a client
1816import 'package:http/http.dart' as http;
1917
20- // The source
21- final userId = Signal(1);
22-
2318// The fetcher
2419Future<String> fetchUser() async {
2520 final response = await http.get(
26- Uri.parse('https://swapi.dev/api/people/${userId.value}/'),
21+ Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
22+ headers: {'Accept': 'application/json'},
2723 );
2824 return response.body;
2925}
3026
31- // The resource (source is optional)
32- final user = Resource(fetchUser, source: userId);
27+ final user = Resource(fetchUser);
28+
29+ // and use it in a widget:
30+ SignalBuilder(
31+ builder: (context, child) {
32+ return user.state.on(
33+ ready: (data) => Text(data),
34+ error: (error, stackTrace) => Text('Error: $error'),
35+ loading: () => const Text('Loading...'),
36+ );
37+ },
38+ ),
39+ ```
40+
41+ A Resource can also be driven by a ` Stream ` .
42+
43+ ``` dart
44+ final stream = Stream.periodic(
45+ const Duration(seconds: 1),
46+ (count) => 'Tick: $count',
47+ );
48+
49+ final resource = Resource.stream(() => stream);
50+
51+ // the widget usage is the same as above, no changes needed
3352```
3453
35- A Resource can also be driven from a ` Stream ` instead of a ` Future ` , and can be created with ` Resource.stream(() => stream) ` .
54+ You may ask yourself: why not just use a ` Future ` or a ` Stream ` directly?
3655
37- The resource has a value named ` ResourceState ` , that provides many useful convenience methods to correctly handle the state of the resource.
56+ ## FutureBuilder is overcomplicated
3857
39- The ` on ` method forces you to handle all the states of a Resource (_ ready_ , _ error_ and _ loading_ ).
40- The are also other convenience methods to handle only specific states:
41- - ` on ` forces you to handle all the states of a Resource
42- - ` maybeOn ` lets you decide which states to handle and provide an ` orElse ` action for unhandled states
43- - ` map ` equal to ` on ` but gives access to the ` ResourceState ` data class
44- - ` maybeMap ` equal to ` maybeOn ` but gives access to the ` ResourceState ` data class
45- - ` isReady ` indicates if the ` Resource ` is in the ready state
46- - ` isLoading ` indicates if the ` Resource ` is in the loading state
47- - ` hasError ` indicates if the ` Resource ` is in the error state
48- - ` asReady ` upcast ` ResourceState ` into a ` ResourceReady ` , or return null if the ` ResourceState ` is in loading/error state
49- - ` asError ` upcast ` ResourceState ` into a ` ResourceError ` , or return null if the ` ResourceState ` is in loading/ready state
50- - ` value ` attempts to synchronously get the value of ` ResourceReady `
51- - ` error ` attempts to synchronously get the error of ` ResourceError `
58+ In my experience, ` FutureBuilder ` is overcomplicated and hard to use correctly.
59+ Even experienced Flutter developers cause side effects, like this:
60+ ``` dart
61+ FutureBuilder<String>(
62+ future: downloadData(), // function where you call your api
63+ ...
64+ ),
65+ ```
5266
53- A ` Resource ` provides the ` fetch ` and ` refresh ` methods.
67+ which causes the ` downloadData ` function to be called multiple times, because the ` build ` method is called multiple times.
68+ Additionally ` AsyncSnapshot ` is a complex class, why do you need to deal with ` ConnectionState ` ? Things are overcomplicated.
5469
55- The ` refresh ` method forces an update and calls the ` fetcher ` function again or subscribes again to the ` stream ` .
70+ ## Stream
5671
72+ Ever wanted to get the latest value of a ` Stream ` synchronously?
73+ You don't have a way, because ` Stream ` is a pipeline, if you start listening to it too later, you miss the previous values.
74+
75+ That's why people starts using ` rxdart ` with the ` BehaviorSubject ` , which is a ` Stream ` that also holds the latest value.
76+
77+ But ` Resource ` will hold the current and also the previous states.
78+
79+ ## Resources can switch the async source at runtime
80+
81+ This is a very powerful feature of ` Resource ` .
82+ You can create a ` Resource ` that depends on another ` Signal ` , and when that ` Signal ` changes, the ` Resource ` will automatically fetch the new data.
83+
84+ For example, you can get the user data based on a user id ` Signal ` :
85+
86+ ``` dart
87+ final userId = Signal<String?>(null);
88+ late final userData = Resource(
89+ () {
90+ return switch (userId.value) {
91+ // If userId is null, return null
92+ null => Future.value(null),
93+ // Otherwise fetch the user data
94+ final id => getUserData(id),
95+ };
96+ },
97+ // Everytime the userId changes, the resource will be triggered to fetch new data
98+ source: userId,
99+ );
100+ ```
101+
102+ This let's you achieve complex async flows with very little code.
103+ The same works for ` Resource.stream ` :
104+
105+ ``` dart
106+ final userId = Signal<String?>(null);
107+ late final userData = Resource.stream(
108+ () {
109+ return switch (userId.value) {
110+ // If userId is null, return an empty stream
111+ null => Stream.value(null),
112+ // Otherwise fetch the user data stream
113+ final id => getUserDataStream(id),
114+ };
115+ },
116+ // Everytime the userId changes, the resource will be triggered to fetch new data
117+ source: userId,
118+ );
119+ ```
120+
121+ If your ` Resource ` depends on multiple ` Signal ` s, you can use a ` Computed ` as source:
122+
123+ ``` dart
124+ final userId = Signal<String?>(null);
125+ final authToken = Signal<String?>(null);
126+ final source = Computed(() => (userId.value, authToken.value));
127+ ```
128+
129+ ## Resource.debounceDelay
130+
131+ You can debounce a ` Resource ` by using ` debounceDelay ` parameter:
132+
133+ ``` dart
134+ Resource(
135+ ...,
136+ debounceDelay: const Duration(seconds: 1),
137+ )
138+ ```
139+
140+ This will wait for 1 second after the last change of the ` source ` before triggering the fetcher.
141+ Use it when you want to avoid multiple calls in a short time, for example when the ` source ` is driven by a text field.
142+
143+ ## Resource.refresh()
144+
145+ You can manually refresh a ` Resource ` by calling the ` refresh ` method.
146+
147+ ## Resource.useRefreshing
148+
149+ By default, when the Resource refreshes, the state will NOT transition to ` loading ` .
150+ Instead, it will set ` isRefreshing ` to true, and the state will remain in the current state (either ` ready ` or ` error ` ).
151+ This allows you to keep showing the current data or error while the new data is being fetched.
152+
153+ ``` dart
154+ SignalBuilder(builder: (context, child) {
155+ final userState = user.state;
156+ return userState.on(
157+ ready: (data) {
158+ return Column(
159+ mainAxisSize: MainAxisSize.min,
160+ children: [
161+ Text(data),
162+ userState.isRefreshing
163+ ? const CircularProgressIndicator()
164+ : ElevatedButton(
165+ onPressed: user.refresh,
166+ child: const Text('Refresh'),
167+ ),
168+ ],
169+ );
170+ },
171+ error: (e, _) {
172+ return Column(
173+ mainAxisSize: MainAxisSize.min,
174+ children: [
175+ Text(e.toString()),
176+ userState.isRefreshing
177+ ? const CircularProgressIndicator()
178+ : ElevatedButton(
179+ onPressed: user.refresh,
180+ child: const Text('Refresh'),
181+ ),
182+ ],
183+ );
184+ },
185+ loading: () => CircularProgressIndicator(),
186+ );
187+ }),
188+ ```
189+
190+ By setting ` useRefreshing ` to false, the state will always transition to ` loading ` when refreshing.
191+ You can also update it globally:
192+ ``` dart
193+ void main() {
194+ SolidartConfig.useRefreshing = false;
195+ runApp(const MyApp());
196+ }
197+ ```
0 commit comments