zino-hofmann / graphql-flutter

A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package.
https://zino-hofmann.github.io/graphql-flutter
MIT License
3.25k stars 622 forks source link

Need example how to combine query and subscription in one widget (documentation improvement) #1255

Open dmitrysdm opened 2 years ago

dmitrysdm commented 2 years ago

Good morning, i'm looking for example, that allow combine query and subscription in one widget (one local state). Processing one common local state is more difficult part for understanding :(

What i want is - a large table with many users, who edit it at the same time.

As result, according Dumb and Smart Components model, i need follow:

Smart Component contain:
  1. query getTable for get / change local state
  2. subscription tableRowUpdated and switch case for update / change local state
  3. mutation addTableRow
  4. mutation updateTableRow
  5. mutation removeTableRow
  6. pass {object table, function addTableRow, function updateTableRow, function removeTableRow} to dumb component
Dumb Component contain:
  1. get {object table, function addTableRow, function updateTableRow, function removeTableRow} from props
  2. render all this as a table

on React, based on hooks implementation looks like:

const Table = () => {
    const [addTableRow] = useMutation(ADDTABLEROW);
    const [updateTableRow] = useMutation(UPDATETABLEROW);
    const [removeTableRow] = useMutation(REMOVETABLEROW);
    const { loading, error, data, subscribeToMore } = useQuery(getTable);

    useEffect(() => {
        const unsubscribe = subscribeToMore({
        document: TABLEROWUPDATED,
        updateQuery: (prev, { subscriptionData }) => {
            if (!subscriptionData.data) {
            return prev;
            } else {
            switch (subscriptionData.data.tableRowUpdated.mutation) {
                case "addTableRow":
                    return Object.assign({}, prev, {...
                case "updateTableRow":
                    return Object.assign({}, prev, {...
                case "removeTableRow":
                    return Object.assign({}, prev, {...
                default:
            }
            }
        },
        });
        return () => {
            unsubscribe();
        };
    }, [subscribeToMore]);
    if (loading) return <p>Loading</p>;
    if (error) return <p>Error(</p>;
    return (
        <TableView
        table={data.getTable}
        addTableRow={addTableRow}
        updateTableRow={updateTableRow}
        removeTableRow={removeTableRow}
      / 
    )
}

switch case by tableRowUpdated.mutation i use for minimize needed resources, and instead of return whole table from server, returns only table row, that updated on client side according mutation

I need for example how to combine query and subscription in one widget at same time, for processing one widget local state.

At same time i am trying to use global state via riverpod provider, and after example with local state processing this, i hope, it will be the same.

dmitrysdm commented 2 years ago

I reread Apollo documentation, subscribeToMore - it how they solve part of this problem (this function used in my example)

dmitrysdm commented 2 years ago

I think i found solution on riverpod main page ( it is contain example for combine two providers, exacly with switch case code example :) ), i think architectural it is correct.

I left this issue open, just for feedback and other opinion, to be sure, that found way is correct.

vincenzopalazzo commented 1 year ago

PRs are always welcome

dmitrysdm commented 1 year ago

I still have not good working solution, and it related (as i think) with issue: #1278 How to dispose subscription request?

Even through riverpod provider, i can't understand how to handle unsubscribe (dispose) after widget was unmount.

Now my schema is follow:

1) GraphQLClient provider (return _client) 2) Repository provider (contain all query mutations and subscriptions, get graphql client from "GraphQLClient provider" )

class UserRepository implements IUserRepository {
  final GraphQLClient client;

  UserRepository({
    required this.client,
  });

  @override
  Future<List<User>> getAllUsers() async {
    final QueryOptions options = QueryOptions(
      document: gql(
        r'''
        query {
          getAllUsers {
            id
            name
            description
          }
        }
        ''',
      ),
    );
    final QueryResult result = await client.query(options);
    final List<dynamic> json = result.data!['getAllUsers'] as List<dynamic>;
    return json.map<User>((element) => User.fromJson(element)).toList();
  }

  @override
  Stream<UserUpdated> userUpdated() async* {
    final SubscriptionOptions options = SubscriptionOptions(
      document: gql(
        r'''
          subscription {
            userUpdated {
              mutation
              user {
                id
                name
                description
              }
            }
          }
        ''',
      ),
    );
    await for (final result in client.subscribe(options)) {
      yield UserUpdated.fromJson(result.data!['userUpdated']);
    }
  }
}

3) StateNotifier provider (get repository from "Repository provider") contein follow code for combine queries and subscriptions:

  Future<void> initUpdatesUser() async {
    state = await repository.getAllUsers();

    var update = repository.userUpdated();
    update.listen((update) {
      switch (update.mutation) {
        case 'addUser':
          state = ...;
          break;
        case 'updateUser':
          state = ...;
          break;
        case 'removeUser':
          state = ...;
          break;
      }
    });
  }

4) UI part (get users list and listen for changes from "StateNotifier provider")

Now it's almost work, on first start all good, but i have a problems when i change route (when my widget unmount/remount), i think the reason for this - is widget/provider dispose handle, because unsubscribe is not exist on "Repository provider" level :(

I have an idea with implementation of an additional provider for updates (I assume that this provider with own dispose, also will do dispose for subscriptions), but it still not tested.

dmitrysdm commented 1 year ago

I have found solution via riverpod (subscriptions dispose was not fixed, but now another errors are epsen), it is sufficient for me.