Skip to content

Commit d3d31e7

Browse files
committed
docs: update resource
1 parent 1d6a158 commit d3d31e7

9 files changed

Lines changed: 190 additions & 35 deletions

File tree

.vscode/launch.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,12 @@
5959
"--dart-define=use_simulated_environment=true"
6060
]
6161
},
62+
{
63+
"name": "Docs",
64+
"cwd": "${workspaceFolder}/docs-v2",
65+
"command": "./node_modules/.bin/astro dev",
66+
"request": "launch",
67+
"type": "node-terminal"
68+
},
6269
]
6370
}

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ final userId = Signal(1);
116116
// The fetcher
117117
Future<String> fetchUser() async {
118118
final response = await http.get(
119-
Uri.parse('https://swapi.dev/api/people/${userId.value}/'),
119+
Uri.parse('https://jsonplaceholder.typicode.com/users/${userId.value}/'),
120+
headers: {'Accept': 'application/json'},
120121
);
121122
return response.body;
122123
}

docs-v2/src/content/docs/learning/resource.mdx

Lines changed: 168 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1412
Let's create a Resource:
1513

1614
```dart
1715
// Using http as a client
1816
import 'package:http/http.dart' as http;
1917
20-
// The source
21-
final userId = Signal(1);
22-
2318
// The fetcher
2419
Future<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+
```

docs/api-docs/resource.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ final userId = Signal(1);
5656
// The fetcher
5757
Future<String> fetchUser() async {
5858
final response = await http.get(
59-
Uri.parse('https://swapi.dev/api/people/${userId.value}/'),
59+
Uri.parse('https://jsonplaceholder.typicode.com/users/${userId.value}/'),
60+
headers: {'Accept': 'application/json'},
6061
);
6162
return response.body;
6263
}

docs/learning/resources.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ final userId = Signal(1);
2323
// The fetcher
2424
Future<String> fetchUser() async {
2525
final response = await http.get(
26-
Uri.parse('https://swapi.dev/api/people/${userId.value}/'),
26+
Uri.parse('https://jsonplaceholder.typicode.com/users/${userId.value}/'),
27+
headers: {'Accept': 'application/json'},
2728
);
2829
return response.body;
2930
}

packages/flutter_solidart/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ final userId = Signal(1);
105105
// The fetcher
106106
Future<String> fetchUser() async {
107107
final response = await http.get(
108-
Uri.parse('https://swapi.dev/api/people/${userId.value}/'),
108+
Uri.parse('https://jsonplaceholder.typicode.com/users/${userId.value}/'),
109+
headers: {'Accept': 'application/json'},
109110
);
110111
return response.body;
111112
}

packages/flutter_solidart/example/lib/pages/resource.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ class _ResourcePageState extends State<ResourcePage> {
1919
await Future.delayed(const Duration(seconds: 2));
2020

2121
final response = await http.get(
22-
Uri.parse('https://swapi.dev/api/people/${userId.value}/'),
22+
Uri.parse('https://jsonplaceholder.typicode.com/users/${userId.value}/'),
23+
headers: {'Accept': 'application/json'},
2324
);
2425
return response.body;
2526
}
@@ -30,7 +31,7 @@ class _ResourcePageState extends State<ResourcePage> {
3031
appBar: AppBar(
3132
title: const Text('Resource'),
3233
),
33-
body: Padding(
34+
body: SingleChildScrollView(
3435
padding: const EdgeInsets.all(16.0),
3536
child: Column(
3637
children: [

packages/solidart/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ final userId = Signal(1);
104104
// The fetcher
105105
Future<String> fetchUser() async {
106106
final response = await http.get(
107-
Uri.parse('https://swapi.dev/api/people/${userId.value}/'),
107+
Uri.parse('https://jsonplaceholder.typicode.com/users/${userId.value}/'),
108+
headers: {'Accept': 'application/json'},
108109
);
109110
return response.body;
110111
}

packages/solidart/lib/src/core/resource.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ part of 'core.dart';
2727
/// // The fetcher
2828
/// Future<String> fetchUser() async {
2929
/// final response = await http.get(
30-
/// Uri.parse('https://swapi.dev/api/people/${userId.value}/'),
30+
/// Uri.parse('https://jsonplaceholder.typicode.com/users/${userId.value}/'),
31+
/// headers: {'Accept': 'application/json'},
3132
/// );
3233
/// return response.body;
3334
/// }

0 commit comments

Comments
 (0)