Skip to content
This repository was archived by the owner on Sep 6, 2021. It is now read-only.

Commit ab39467

Browse files
committed
add "Structuring Queries" page
1 parent 91a2176 commit ab39467

File tree

2 files changed

+231
-1
lines changed

2 files changed

+231
-1
lines changed

docs/structuring-queries.md

+230
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
---
2+
id: structuring-queries
3+
title: Structuring Queries
4+
---
5+
6+
Since Flutter is built around the principle of Widget composition, it's common to have data spread across many nested widgets. For example, let's say we want to have a `PokemonList` Widget that displays a list of `PokemonCard` Widgets.
7+
8+
Our Widget tree might look like this:
9+
10+
```
11+
MaterialApp
12+
PokemonList
13+
PokemonCard
14+
PokemonCard
15+
PokemonCard
16+
```
17+
18+
Our first impulse might be to write a Query like this one:
19+
20+
```graphql
21+
query AllPokemon {
22+
pokemons {
23+
id
24+
name
25+
avatar
26+
}
27+
}
28+
```
29+
30+
And use it in the following Widgets:
31+
32+
```dart
33+
import 'package:flutter/material.dart';
34+
import 'package:ferry/ferry.dart';
35+
import 'package:ferry_flutter/ferry_flutter.dart';
36+
import 'package:get_it/get_it.dart';
37+
import 'package:built_collection/built_collection.dart';
38+
39+
import './graphql/all_pokemon.data.gql.dart';
40+
import './graphql/all_pokemon.req.gql.dart';
41+
import './graphql/all_pokemon.var.gql.dart';
42+
43+
class PokemonList extends StatelessWidget {
44+
final client = GetIt.I<Client>();
45+
46+
@override
47+
Widget build(BuildContext context) {
48+
return Scaffold(
49+
appBar: AppBar(
50+
title: Text('Pokemon List'),
51+
),
52+
body: Operation<GAllPokemonData, GAllPokemonVars>(
53+
client: client,
54+
operationRequest: GAllPokemonReq(),
55+
builder: (context, response, error) {
56+
if (response!.loading)
57+
return Center(child: CircularProgressIndicator());
58+
59+
final pokemons = response.data?.pokemons? ?? BuiltList();
60+
61+
return ListView.builder(
62+
itemCount: pokemons.length,
63+
itemBuilder: (context, index) => PokemonCard(
64+
pokemon: pokemons[index],
65+
),
66+
);
67+
},
68+
),
69+
);
70+
}
71+
}
72+
73+
class PokemonCard extends StatelessWidget {
74+
final GAllPokemonData_pokemons pokemon;
75+
76+
const PokemonCard({required this.pokemon});
77+
78+
@override
79+
Widget build(BuildContext context) {
80+
return Card(
81+
child: InkWell(
82+
onTap: () => Navigator.of(context)
83+
.pushNamed('detail', arguments: {'id': pokemon.id}),
84+
child: Padding(
85+
padding: const EdgeInsets.all(8.0),
86+
child: Column(
87+
children: <Widget>[
88+
SizedBox(
89+
child: Ink.image(image: NetworkImage(pokemon.avatar)),
90+
height: 200,
91+
width: 200,
92+
),
93+
Text(
94+
pokemon.name,
95+
style: Theme.of(context).textTheme.headline6,
96+
),
97+
],
98+
),
99+
),
100+
),
101+
);
102+
}
103+
}
104+
```
105+
106+
While this works, it tightly couples our `PokemonList` and `PokemonCard` Widgets which causes several disadvantages:
107+
108+
1. Our `PokemonCard` Widget can't be reused with data from other GraphQL Operations since it has an explicit dependency on the `GAllPokemonData_pokemons` type.
109+
2. Our `AllPokemon` Query must keep track of the data requirements not only for our `PokemonList` itself (in which the query is executed), but also for all child Widgets (i.e. `PokemonCard`).
110+
111+
## Colocation of Widgets and Data Requirements
112+
113+
A common pattern to overcome these issues is to _colocate_ Widgets and their data requirements. In other words, each Widget should have a corresponding GraphQL definition that specifies only the data needed for that Widget.
114+
115+
A naive implementation of this (don't do this) might be to:
116+
117+
1. Request only the `id` field in our `AllPokemon` Query
118+
2. Pass the `id` to the `PokemonCard`
119+
3. Execute a `GetPokemon` Query in our `PokemonCard` that fetches the data only for that Pokemon
120+
121+
However, this would result in a seperate network request (and subsequent database query) for each Pokemon in the list. Not very efficient.
122+
123+
Istead, we can extract our `PokemonCard`'s data requirements into a Fragment:
124+
125+
```graphql
126+
fragment PokemonCardFragment on Pokemon {
127+
id
128+
name
129+
avatar
130+
}
131+
```
132+
133+
```graphql
134+
# import './pokemon_card_fragment.graphql'
135+
136+
query AllPokemon {
137+
pokemons {
138+
...PokemonCardFragment
139+
}
140+
}
141+
```
142+
143+
Now our `PokemonCard` can depend on the `GPokemonCardFragment` type.
144+
145+
```dart {2}
146+
import 'package:flutter/material.dart';
147+
import './graphql/pokemon_card_fragment.data.gql.dart';
148+
149+
class PokemonCard extends StatelessWidget {
150+
final GPokemonCardFragment pokemon;
151+
152+
const PokemonCard({required this.pokemon});
153+
154+
@override
155+
Widget build(BuildContext context) {
156+
return Card(
157+
child: InkWell(
158+
onTap: () => Navigator.of(context)
159+
.pushNamed('detail', arguments: {'id': pokemon.id}),
160+
child: Padding(
161+
padding: const EdgeInsets.all(8.0),
162+
child: Column(
163+
children: <Widget>[
164+
SizedBox(
165+
child: Ink.image(image: NetworkImage(pokemon.avatar)),
166+
height: 200,
167+
width: 200,
168+
),
169+
Text(
170+
pokemon.name,
171+
style: Theme.of(context).textTheme.headline6,
172+
),
173+
],
174+
),
175+
),
176+
),
177+
);
178+
}
179+
}
180+
```
181+
182+
This means the `PokemonCard` Widget can be reused anywhere the `PokemonCardFragment` is used. It also means that if our data requirements for `PokemonCard` change (say, if we need to add a `height` property), we only need to update our `PokemonCardFragment`. Our `AllPokemon` Query and any other operations that use `PokemonCardFragment` don't need to be updated.
183+
184+
This pattern leads to code that is easier to maintain, test, and reason about.
185+
186+
## Fragments on Root Query
187+
188+
The above pattern works even if your data requriements for a single screen include multiple GraphQL queries since you can include Fragments on any GraphQL type, including the root `Query` type.
189+
190+
For example, let's say you want to add a user avatar Widget to the header of your `PokemonListScreen` that shows the currently logged-in user.
191+
192+
```
193+
MaterialApp
194+
PokemonListScreen
195+
UserAvatar
196+
PokemonList
197+
PokemonCard
198+
PokemonCard
199+
PokemonCard
200+
```
201+
202+
You might structure your queries like so:
203+
204+
```graphql
205+
fragment PokemonCardFragment on Pokemon {
206+
id
207+
name
208+
avatar
209+
}
210+
211+
fragment PokemonListFragment on Query {
212+
pokemons {
213+
...PokemonCardFragment
214+
}
215+
}
216+
217+
fragment UserAvatarFragment on Query {
218+
user(id: $userId) {
219+
id
220+
avatar
221+
}
222+
}
223+
224+
query PokemonListScreenQuery($userId: ID!) {
225+
...PokemonListFragment
226+
...UserAvatarFragment
227+
}
228+
```
229+
230+
Even though you are fetching data from two different root queries (`pokemons` and `user`), you can use a single `Operation` Widget which will make a single network request for the `PokemonListScreen`.

sidebars.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module.exports = {
44
'Getting Started': ['setup', 'codegen'],
55
Fetching: ['queries', 'mutations', 'fetch-policies', 'pagination', 'error-handling'],
66
Caching: ['cache-configuration', 'cache-interaction', 'garbage-collection'],
7-
Flutter: ['flutter', 'flutter-operation-widget'],
7+
Flutter: ['flutter', 'flutter-operation-widget', 'structuring-queries'],
88
Advanced: ['custom-scalars', 'customization'],
99
},
1010
};

0 commit comments

Comments
 (0)