sling_gql_hooks 1.0.1
sling_gql_hooks: ^1.0.1 copied to clipboard
flutter_hooks adapter for sling_gql: useSlingQuery, useSlingMutation and useSlingSubscription, the QueryBuilder / MutationBuilder / SubscriptionBuilder of a HookWidget.
// A self-contained sling_gql app written with hooks: one query
// (useSlingQuery), one mutation with an optimistic update
// (useSlingMutation). Point [endpoint] at a server exposing the schema below
// and run it.
//
// In a real app the `Query` / `User` / `Mutation` classes at the bottom of
// this file are generated by `sling_gql_gen` from your schema's introspection
// and imported from there. They are written out here so the example runs on
// its own.
//
// type Query { me: User! user(id: ID!): User }
// type User { id: ID! name: String! age: Int friends(limit: Int): [User!]! }
// type Mutation { rename(id: ID!, name: String!): User! }
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:sling_gql/sling_gql.dart';
import 'package:sling_gql_hooks/sling_gql_hooks.dart';
final endpoint = Uri.parse('https://example.com/graphql');
void main() {
final client = SlingClient<Query>(endpoint: endpoint, schema: slingSchema);
runApp(
SlingScope<Query>(
client: client,
child: const MaterialApp(home: ProfilePage()),
),
);
}
/// Reads `me`, its name, age and first three friends. The fields read in the
/// selector become the query, fetched once at the end of the frame; the
/// widget rebuilds when the data lands.
class ProfilePage extends HookWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
// Read everything the build needs inside the selector, unconditionally —
// a field read only behind an `if` costs a second round trip.
final ((me, name, age, friendNames), state) = useSlingQuery(
(Query q) => (
q.me,
q.me.name,
q.me.age,
q.me.friends(limit: 3).map((f) => f.name).toList(),
),
);
return Scaffold(
appBar: AppBar(title: const Text('sling_gql_hooks')),
body: state.error != null
? Center(
child: TextButton(
onPressed: state.refetch,
child: Text('Failed: ${state.error}\nTap to retry'),
),
)
// Before the response arrives, scalars are null and lists have
// one skeleton element, so this renders as a placeholder.
: ListView(
children: [
ListTile(
title: Text(name ?? '…'),
subtitle: Text(age == null ? '' : '$age years old'),
trailing: state.isLoading
? const CircularProgressIndicator.adaptive()
: RenameButton(user: me),
),
const Divider(),
for (final friendName in friendNames)
ListTile(
leading: const Icon(Icons.person),
title: Text(friendName ?? '…'),
),
],
),
);
}
}
/// Runs the `rename` mutation. The response is normalized into the same
/// cache entity (`User:<id>`), so every widget showing this user rebuilds by
/// itself; the optimistic write shows the new name immediately and is rolled
/// back if the request fails.
class RenameButton extends HookWidget {
const RenameButton({super.key, required this.user});
final User user;
@override
Widget build(BuildContext context) {
// Reads through an accessor handed down by ProfilePage, like a
// QueryBuilder's child widget: `id` is selected with every keyed type and
// `name` was read in the selector, so both are already cached.
final id = user.id;
final currentName = user.name;
final (mutate, state) = useSlingMutation<Mutation>();
return IconButton(
icon: const Icon(Icons.edit),
onPressed: state.isLoading || id == null
? null
: () async {
final newName = await askForName(context, currentName ?? '');
if (newName == null) return;
await mutate(
(m) => m.rename(id: id, name: newName)?.name,
optimistic: () => user.name = newName,
);
},
);
}
}
Future<String?> askForName(BuildContext context, String initial) {
final controller = TextEditingController(text: initial);
return showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Rename'),
content: TextField(controller: controller, autofocus: true),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, controller.text),
child: const Text('Save'),
),
],
),
);
}
// ---------------------------------------------------------------------------
// What `sling_gql_gen` generates for the schema above. Do not write this by
// hand in your app.
// ---------------------------------------------------------------------------
class Query extends Accessor {
Query(super.recorder, super.selection, super.path);
Query.root(Recorder r) : super(r, r.root, const []);
User get me => object('me', User.new, keyed: true)!;
User? user({required String id}) =>
object('user', User.new, args: {'id': Arg('ID!', id)}, lookup: 'User');
}
class User extends Accessor {
User(super.recorder, super.selection, super.path);
String? get id => scalar<String>('id');
String? get name => scalar<String>('name');
set name(String? v) => write('name', v);
int? get age => scalar<int>('age');
List<User> friends({int? limit}) => list(
'friends',
User.new,
args: {'limit': Arg('Int', limit)},
keyed: true,
)!;
}
class Mutation extends Accessor {
Mutation(super.recorder, super.selection, super.path);
Mutation.root(Recorder r) : super(r, r.root, const []);
User? rename({required String id, required String name}) => object(
'rename',
User.new,
args: {'id': Arg('ID!', id), 'name': Arg('String!', name)},
keyed: true,
);
}
const slingSchema = SlingSchema<Query, Mutation>(
query: Query.root,
mutation: Mutation.root,
);