gql_link
를 도식화 한 그림인데, 나도 모든 link에 대해서는 사용해보지는 않았고, token을 전달하는 AuthLink
, Http 통신을 로깅하기 위해서 HttpLink
, 소켓 사용 시에 필요한 WsLink
정도만 사용해보았다.final graphQLClientProvider = Provider<GraphQLClient>((ref) {
/**
* link를 하는 순서 중요
*/
final HttpLink httpLink = HttpLink(dotenv.env['SERVER_URL'] ?? "",
httpClient: LoggerHttpClient(http.Client()));
final WebSocketLink webSocketLink = WebSocketLink(
dotenv.env['WEBSOCKET_URL'] ?? "",
config: SocketClientConfig(
autoReconnect: true,
delayBetweenReconnectionAttempts: const Duration(milliseconds: 100),
initialPayload: () async {
final String idToken = await getIdToken();
return {
"Authorization": "Bearer $idToken",
};
},
),
subProtocol: GraphQLProtocol.graphqlTransportWs,
);
final AuthLink authLink = AuthLink(
getToken: () async {
try {
String idToken = await getIdToken();
AppLogger.info("Bearer $idToken");
return 'Bearer $idToken';
} catch (e) {
AppLogger.debug(e.toString());
}
return null;
},
);
Link link = authLink.concat(httpLink);
link = Link.split(
(request) => request.isSubscription,
webSocketLink,
link,
);
return GraphQLClient(
cache: GraphQLCache(store: HiveStore()),
link: link,
);
});
class LoggerHttpClient extends BaseClient {
LoggerHttpClient(this._client);
final Client _client;
final JsonEncoder _encoder = const JsonEncoder.withIndent(' ');
final JsonDecoder _decoder = const JsonDecoder();
void close() {
_client.close();
}
Future<StreamedResponse> send(BaseRequest request) {
return _client.send(request).then((StreamedResponse response) async {
final String responseString = await response.stream.bytesToString();
AppLogger.info('''
=> request: ${response.request.toString()},
=> headers: ${_encoder.convert(response.headers)},
<- statusCode: ${response.statusCode},
<- responseString: ${_encoder.convert(_decoder.convert(responseString))},
''');
return StreamedResponse(
ByteStream.fromBytes(utf8.encode(responseString)),
response.statusCode,
headers: response.headers,
reasonPhrase: response.reasonPhrase,
persistentConnection: response.persistentConnection,
contentLength: response.contentLength,
isRedirect: response.isRedirect,
request: response.request,
);
});
}
}
grphql
라이브러리를 사용하는데 왜 해당 라이브러리를 선택하게 되었는지에 대해서도 공유하려고 한다.Query, Mutation, Subscription
이다. (여기서 각각의 용어가 생소하다면 단순하게 설명하자면 다음과 같다.)1. GET - QUERY
2. DELETE - MUTATION
3. POST - MUTATION
4. PUT - MUTATION
5. SOCKET - SUBSCRIPTION
String readRepositories = """
query ReadRepositories(\$nRepositories: Int!) {
viewer {
repositories(last: \$nRepositories) {
nodes {
id
name
viewerHasStarred
}
}
}
}
""";
// ...
Query(
options: QueryOptions(
document: gql(readRepositories), // this is the query string you just created
variables: {
'nRepositories': 50,
},
pollInterval: const Duration(seconds: 10),
),
// Just like in apollo refetch() could be used to manually trigger a refetch
// while fetchMore() can be used for pagination purpose
builder: (QueryResult result, { VoidCallback? refetch, FetchMore? fetchMore }) {
if (result.hasException) {
return Text(result.exception.toString());
}
if (result.isLoading) {
return const Text('Loading');
}
List? repositories = result.data?['viewer']?['repositories']?['nodes'];
if (repositories == null) {
return const Text('No repositories');
}
return ListView.builder(
itemCount: repositories.length,
itemBuilder: (context, index) {
final repository = repositories[index];
return Text(repository['name'] ?? '');
});
},
);
// ...
useQuery
와 같은 훅이다. (실제로 flutter hooks로 이 형태를 사용한다면 useQuery 훅이다.)String addStar = """
mutation AddStar(\$starrableId: ID!) {
addStar(input: {starrableId: \$starrableId}) {
starrable {
viewerHasStarred
}
}
}
""";
...
Mutation(
options: MutationOptions(
document: gql(addStar), // this is the mutation string you just created
// you can update the cache based on results
update: (GraphQLDataProxy cache, QueryResult result) {
return cache;
},
// or do something with the result.data on completion
onCompleted: (dynamic resultData) {
print(resultData);
},
),
builder: (
RunMutation runMutation,
QueryResult result,
) {
return FloatingActionButton(
onPressed: () => runMutation({
'starrableId': <A_STARTABLE_REPOSITORY_ID>,
}),
tooltip: 'Star',
child: Icon(Icons.star),
);
},
);
...
final subscriptionDocument = gql(
r'''
subscription reviewAdded {
reviewAdded {
stars, commentary, episode
}
}
''',
);
class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Subscription(
options: SubscriptionOptions(
document: subscriptionDocument,
),
builder: (result) {
if (result.hasException) {
return Text(result.exception.toString());
}
if (result.isLoading) {
return Center(
child: const CircularProgressIndicator(),
);
}
// ResultAccumulator is a provided helper widget for collating subscription results.
// careful though! It is stateful and will discard your results if the state is disposed
return ResultAccumulator.appendUniqueEntries(
latest: result.data,
builder: (context, {results}) => DisplayReviews(
reviews: results.reversed.toList(),
),
);
}
),
)
);
}
}
flutter hooks
라이브러리를 통해 정말 react와 유사하게(라이브러리 자체가 flutter를 hooks기반으로 동작하게 하려는 것이 목적이기에( useQuery, useMutation
과 같은 훅을 사용해서 graphql을 사용 할 수 있는 방법이 존재하고 라이브러리에서도 소개를 해주고 있다.const String readRepositories = r'''
query ReadRepositories($nRepositories: Int!) {
viewer {
repositories(last: $nRepositories) {
nodes {
__typename
id
name
viewerHasStarred
}
}
}
}
''';
// ...
const int nRepositories = 50;
final QueryOptions options = QueryOptions(
document: gql(readRepositories),
variables: <String, dynamic>{
'nRepositories': nRepositories,
},
);
final QueryResult result = await client.query(options);
if (result.hasException) {
print(result.exception.toString());
}
final List<dynamic> repositories =
result.data['viewer']['repositories']['nodes'] as List<dynamic>;
// ...
const String addStar = r'''
mutation AddStar($starrableId: ID!) {
action: addStar(input: {starrableId: $starrableId}) {
starrable {
viewerHasStarred
}
}
}
''';
// ...
final MutationOptions options = MutationOptions(
document: gql(addStar),
variables: <String, dynamic>{
'starrableId': repositoryID,
},
);
final QueryResult result = await client.mutate(options);
if (result.hasException) {
print(result.exception.toString());
return;
}
final bool isStarred =
result.data['action']['starrable']['viewerHasStarred'] as bool;
if (isStarred) {
print('Thanks for your star!');
return;
}
// ...
final subscriptionDocument = gql(
r'''
subscription reviewAdded {
reviewAdded {
stars, commentary, episode
}
}
''',
);
// graphql/client.dart usage
subscription = client.subscribe(
SubscriptionOptions(
document: subscriptionDocument
),
);
subscription.listen(reactToAddedReview)
graphql_flutter
라이브러리와 graphql
라이브러리의 가장 큰 차이점이라고 한다면, 위젯에 바인딩 되는지의 여부. 그리고 lazy하게 호출 할 수 있는 지의 여부이다.graphql_flutter
라이브러리에 lazy한 옵션이 있으면 더 좋을 것 같다. 아니면 flutter hooks
라이브러리에 useLazyQuery
같은 옵션이 있다면 더 좋을 것 같다.graphql
라이브러리로 통합했었던것 같다.flutter hooks
를 적극적으로 사용했으면 어땠을까 라는 생각이 든다.