sling_gql 0.1.1 copy "sling_gql: ^0.1.1" to clipboard
sling_gql: ^0.1.1 copied to clipboard

GraphQL client for Flutter where the widget is the query — read a field, get the query. Proof of concept.

example/main.dart

// A complete, self-contained sling_gql app: one query, one mutation, one
// optimistic update. 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
// (`dart run sling_gql_gen --schema schema.json --out lib/generated/schema.dart`)
// and imported from there. They are written out here so the example runs on
// its own. A larger app (pagination, favourites, network log) lives in the
// repository's `example/` directory.
//
//   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:sling_gql/sling_gql.dart';

final endpoint = Uri.parse('https://example.com/graphql');

void main() {
  final client = SlingClient<Query>(
    endpoint: endpoint,
    rootFactory: Query.root,
  );
  runApp(
    SlingScope<Query>(
      client: client,
      schema: slingSchema,
      child: const MaterialApp(home: ProfilePage()),
    ),
  );
}

/// Reads `me`, its name, age and first three friends. Nothing is declared up
/// front: the fields read during `build` become the query, fetched once at
/// the end of the frame, and the widget rebuilds when the data lands.
class ProfilePage extends StatelessWidget {
  const ProfilePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('sling_gql')),
      body: QueryBuilder<Query>(
        builder: (context, query, state) {
          // Read everything you need at the top of build, unconditionally —
          // a field read only inside an `if` costs a second round trip.
          final me = query.me;
          final name = me.name;
          final age = me.age;
          final friends = me.friends(limit: 3);

          if (state.error != null) {
            return 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 the layout below renders as a placeholder.
          return 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 friend in friends)
                ListTile(
                  leading: const Icon(Icons.person),
                  title: Text(friend.name ?? '…'),
                ),
            ],
          );
        },
      ),
    );
  }
}

/// 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 StatelessWidget {
  const RenameButton({super.key, required this.user});

  final User user;

  @override
  Widget build(BuildContext context) {
    final id = user.id;
    final currentName = user.name;
    return MutationBuilder<Mutation>(
      builder: (context, mutate, state) => 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');
  set age(int? v) => write('age', v);
  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,
  );
}

extension SlingCacheAccess on CacheScope<Query> {
  User? user(String id) => entity('User', id, User.new);
}

const slingSchema = SlingSchema<Query, Mutation>(
  query: Query.root,
  mutation: Mutation.root,
);
0
likes
160
points
--
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

GraphQL client for Flutter where the widget is the query — read a field, get the query. Proof of concept.

Homepage
Repository (GitHub)
View/report issues

Topics

#graphql #client #cache #codegen

License

MIT (license)

Dependencies

flutter, http

More

Packages that depend on sling_gql