amazon-web-servicesdartwebsocketaws-appsyncgraphql-flutter

aws appsync subscription not working with flutter graphql_flutter package


my pubspec.yaml

dev_dependencies:
  flutter_test:
    sdk: flutter
  graphql_flutter: ^4.0.0

roomctrl.dart

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:innospace/config/appsync-service.dart';
import 'package:innospace/config/client_provider.dart';

class RoomCtrl extends StatefulWidget {
  RoomCtrl({Key key}) : super(key: key);

  _RoomCtrlState createState() => _RoomCtrlState();
}

class _RoomCtrlState extends State<RoomCtrl> {

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ClientProvider(
        child: Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
         Subscription(
        options:SubscriptionOptions(
            document: gql(AppSyncService.onUpdateStateTestSubscription)),
          builder: (result) {
            if (result.hasException) {
              return Text(result.exception.toString());
            }

            if (result.isLoading) {
              return Center(
                child: const CircularProgressIndicator(),
              );
            }
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  Container(
                    child: Text(result.data.length.toString()),
                  )
                ],
              ),
            );
          },
        )

          ],
        ),
      ),
    ));
  }
}

client_provider.dart

import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:flutter/material.dart';

import 'constants.dart';

String uuidFromObject(Object object) {
  if (object is Map<String, Object>) {
    final String typeName = object['__typename'] as String;
    final String id = object['id'].toString();
    if (typeName != null && id != null) {
      return <String>[typeName, id].join('/');
    }
  }
  return null;
}

ValueNotifier<GraphQLClient> clientFor() {
   const dynamic headers = {
    "headers": {
      "host": AWS_APP_SYNC_ENDPOINT_AUTHORITY,
      "x-api-key": AWS_APP_SYNC_KEY
    }};
   const  sClient= SocketClientConfig(
      autoReconnect : true,
      initialPayload: headers
  );

   final WebSocketLink _webSocketLink =new WebSocketLink(AWS_APP_SYNC_ENDPOINT_WSS,  config:sClient );

   final Link link = _webSocketLink;
  return ValueNotifier<GraphQLClient>(
    GraphQLClient(
      cache: GraphQLCache(),
      link: link,
    ),
  );
}

/// Wraps the root application with the `graphql_flutter` client.
/// We use the cache for all state management.
class ClientProvider extends StatelessWidget {
  ClientProvider({
    @required this.child,
  }) : client = clientFor();

  final Widget child;
  final ValueNotifier<GraphQLClient> client;

  @override
  Widget build(BuildContext context) {
    return GraphQLProvider(
      client: client,
      child: child,
    );
  }
}

constants.dart

[https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html][1]

const AWS_APP_SYNC_ENDPOINT_AUTHORITY = "xxxxxxxxxxxxxxxxxxxxx.appsync-api.ap-southeast-2.amazonaws.com"; 
const AWS_APP_SYNC_ENDPOINT_WSS = "wss://xxxxxxxxxxxxxxxxxxxxx.appsync-realtime-api.ap-southeast-2.amazonaws.com/graphql?header=base64encryption(xxx-xxxxxxxxxxxxxxxxxx as per aws)&payload=e30="; 
const AWS_APP_SYNC_KEY = "xxx-xxxxxxxxxxxxxxxxxx";

subscription query

static String onUpdateStateTestSubscription = '''
        subscription OnUpdateStateTest {
        onUpdateStateTest {
          __typename
          RoomId
          RoomName
        }
      }''';

console

Connecting to websocket: wss://xxxxxxxxxxxxxxxxxxxxx.appsync-realtime-api.ap-southeast-2.amazonaws.com/graphql?header=base64encryption(xxx-xxxxxxxxxxxxxxxxxx as per aws)&payload=e30=...
I/flutter ( 9942): Connected to websocket.
I/flutter ( 9942): Haven't received keep alive message for 30 seconds. Disconnecting..
I/flutter ( 9942): Disconnected from websocket.
I/flutter ( 9942): Scheduling to connect in 5 seconds...
I/flutter ( 9942): Connecting to websocket: wss://xxxxxxxxxxxxxxxxxxxxx.appsync-realtime-api.ap-southeast-2.amazonaws.com/graphql?header=base64encryption(xxx-xxxxxxxxxxxxxxxxxx as per aws)&payload=e30=...
I/flutter ( 9942): Connected to websocket.
I/flutter ( 9942): Haven't received keep alive message for 30 seconds. Disconnecting..
I/flutter ( 9942): Disconnected from websocket.
I/flutter ( 9942): Scheduling to connect in 5 seconds...

Finally output on mobile is showing loading gif i.e CircularProgressIndicator(), means returns always true at "if (result.isLoading)"

[![enter image description here][2]][2]

Could anyone please help!!

note: the same appsync working perfectly in angular application. [1]: https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html [2]: https://i.sstatic.net/59KZh.png


Solution

  • See my recent update at the bottom.

    I'm actually running into the same problem. I don't have a complete answer but I feel like this is a clue to a partial answer.

    From the link:

    As far as I know, AppSync doesn't use wss://<your_end_point>. I have tried this before, and my http connection was never "upgraded to a wss" connection. I had to POST a request to the http endpoint requesting a subscription connection, then AWS returned connection details containing a new endpoint (a wss url), a topic and a client id. Only then could I connect to the socket using using a third party library (using the URL AWS returned). I had to dig into their official sdk to find this out, and then through trial and error to get it to work.

    I've confirmed w/ the GraphiQL tool that I do get a response when I POST my subscription graphql blob. I get a websocket url back along w/ some other items in JSON. I haven't figured out how to capture that response in Flutter yet but I'll update this if I find that piece as well.

    From there, I believe we'd need to dynamically create the websocket from that new url and send our subscription graphql blob to that.

    Absolutely, this is convoluted but it's the path I'm proceeding with for now.

    UPDATE: AWS Amplify for Flutter now supports subscriptions! I am using a hybrid solution. I have flutter_graphql and artemis for mutations/queries. For subscriptions I am using amplify_api: '<1.0.0'. It was just released in the past weeks. It actually only took me a few minutes to get it working. Amplify automatically does the URL resolution and authentication for you.

    Link to official docs

    You'll need to generate an amplifyconfiguration.dart that is specific for your AWS endpoint and add it to your project. The AWS docs cover this and the person that setup your AWS endpoint should know exactly what you need.

    Example from link:

    try {
        String graphQLDocument = '''subscription OnCreateTodo {
            onCreateTodo {
              id
              name
              description
            }
          }''';
    
        var operation = Amplify.API.subscribe(
            request: GraphQLRequest<String>(document: graphQLDocument),
            onData: (event) {
              print('Subscription event data received: ${event.data}');
            },
            onEstablished: () {
              print('Subscription established');
            },
            onError: (e) {
              print('Subscription failed with error: $e');
            },
            onDone: () {
              print('Subscription has been closed successfully');
            });
    } on ApiException catch (e) {
        print('Failed to establish subscription: $e');
    }
    

    Don't forget to unsubscribe:

    // Cancel the subscription when you're finished with it
    operation.cancel();