amplify_flutterでAWS AppSyncのSubscriptionを試す

2023/06/25に公開

1.はじめに

草野球して昼からビール飲んで、プロ野球のデーゲーム見て、夜にZennの記事を書く。そんな日曜日があってもいい。

さて。
10年ほど前、WebSocketがRFC勧告(RFC6455)されたとき、えー何それすごい、と思ってワクワクした記憶があります。

個人的に最初に恩恵を受けた(自分が関与する開発としてね)のは、FirebaseのFirestoreです。
Firestoreのリアルタイム更新の機能が強力で、簡単にUXを向上させることができました。
初めてドキュメントベースのDBを利用したので、そこは苦労しましたが。

今回は、AWS AppSyncを利用してサーバ側の更新をSubscribeしてみたいと思います。
簡単に開始できそうなDynamoDBを利用しましたが、AppSyncは他にも色々なAWSサービスにアタッチできそうなので、拡張の幅が広そうです。

IoTのエッジデバイスのデータをMQTTでPublishしてAppSyncへMutationしておき、それをアプリからSubscribeする、みたいなことができそうです。そういう面白そうなことをやりたいですよね。
業務業務したWeb画面とかもう嫌で...

タイトルの通り、amplify_flutterでAWS AppSyncのSubscriptionを試します。

2.ゴール

GitHubにソースを置きました。動画も貼り付けてあります。

https://github.com/motucraft/appsync

3.AppSyncリソース作成

API作成を選択します。

Design from scratchを選択しました。

API名は「sample_appsync」としました。

ここでは、AppSyncのリソース作成と同時に、DynamoDBのテーブルを作成します。
フィールドには、id、messageを用意し、idをPrimary keyとしました。

確認ページが表示されるため、「APIを作成」を選択してリソースを作成します。

リソースが作成されると、設定からGraphQLのエンドポイントやAPIキーを確認することができます。(このリソースは既に削除済みですので利用できません)

今回はSuscribeしたいので、以下を利用します。

4.Flutter

利用したパッケージは以下のとおりです。

name: appsync
description: A new Flutter project.
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=3.0.5 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2

  amplify_flutter: ^1.2.0
  amplify_api: ^1.2.0

  simple_logger: ^1.9.0+2
  flutter_hooks: ^0.18.6
  hooks_riverpod: ^2.3.6
  riverpod_annotation: ^2.1.1
  freezed_annotation: ^2.2.0
  json_annotation: ^4.8.1

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0
  build_runner: ^2.4.5
  freezed: ^2.3.5
  json_serializable: ^6.7.0
  riverpod_generator: ^2.2.3
  custom_lint: ^0.4.0
  riverpod_lint: ^1.3.2

flutter:
  uses-material-design: true

main.dartです。ここで、Amplifyのconfigureを実行しています。

import 'package:amplify_api/amplify_api.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'amplifyconfiguration.dart';
import 'sample_app_sync.dart';
import 'util/logger.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  try {
    await Amplify.addPlugins([AmplifyAPI()]);
    await Amplify.configure(amplifyConfig);
    logger.info('Amplify configured.');
  } catch (error, stack) {
    logger.severe(error);
    logger.severe(stack);
  }

  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'AppSync Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const SampleAppSync(),
    );
  }
}

amplifyconfiguration.dartです。Amplifyの公式ドキュメントに説明があります。今回は、Manual Configurationを利用しています。

/// The AWS resources have been deleted, so this information is not usable for anything.
const amplifyConfig = '''{
  "api": {
    "plugins": {
      "awsAPIPlugin": {
        "sample_appsync_subscription": {
          "endpointType": "GraphQL",
          "endpoint": "https://yaqix2nemvbyln2l6g747gd7gq.appsync-api.ap-northeast-1.amazonaws.com/graphql",
          "region": "ap-northeast-1",
          "authorizationType": "API_KEY",
          "apiKey": "da2-blxk65ccrrc5vnz5utew4vebee"
        }
      }
    }
  }
}''';

const graphQL = '''subscription sample_appsync_subscription {
      onCreateSample_appsync_subscription {
        id
        message
      }
    }''';

sample_app_sync.dartです。ここで、Subscribeした値を表示しています。RiverpodのStreamProviderを利用しています。

import 'dart:convert';

import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'amplifyconfiguration.dart';
import 'message.dart';
import 'util/logger.dart';

part 'sample_app_sync.g.dart';

class SampleAppSync extends HookWidget {
  const SampleAppSync({super.key});

  
  Widget build(BuildContext context) {
    final messages = useState<List<Message>>([]);

    return Scaffold(
      appBar: AppBar(
        title: const Text('AppSync Demo'),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            Consumer(
              builder: (_, ref, __) {
                ref.listen(subscriptionProvider, (_, next) {
                  if (next is AsyncData && next.value != null && next.value!.data != null) {
                    final decodedJson = json.decode(next.value!.data!);
                    final message = Wrapper.fromJson(decodedJson).onCreateSample_appsync_subscription;
                    messages.value = [...messages.value, message];
                  }
                });

                logger.info('messages=${messages.value}');

                return ListView.builder(
                  shrinkWrap: true,
                  itemCount: messages.value.length,
                  itemBuilder: (BuildContext context, int index) {
                    final message = messages.value[index];
                    return ListTile(
                      title: Text(message.message),
                      subtitle: Text(message.id, style: const TextStyle(fontSize: 12)),
                    );
                  },
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}


Stream<GraphQLResponse<String>> subscription(SubscriptionRef ref) {
  return Amplify.API.subscribe(
    GraphQLRequest<String>(document: graphQL),
    onEstablished: () => logger.info('Subscription established'),
  );
}

あとは、データクラスです。JSONで取れてくるので、freezedを使ってJSONからオブジェクトへ変換しています。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'message.freezed.dart';
part 'message.g.dart';


class Wrapper with _$Wrapper {
  const factory Wrapper({
    required Message onCreateSample_appsync_subscription,
  }) = _Wrapper;

  factory Wrapper.fromJson(Map<String, Object?> json) => _$WrapperFromJson(json);
}


class Message with _$Message {
  const factory Message({
    required String id,
    required String message,
  }) = _Message;

  factory Message.fromJson(Map<String, Object?> json) => _$MessageFromJson(json);
}

たったこれだけでSubscribeできてしまいます。あとはAWSコンソールのクエリから、createSample_appsync_subscriptionを実行して確認しました。
その様子は、GitHubに置いた動画にて確認できます。

https://github.com/motucraft/appsync/assets/35750184/c3fa9ee6-fa99-4407-9ca5-cf2f12a59882

5.おわりに

amplify_flutterでAWS AppSyncのSubscriptionを試すことができました。
私はGraphQLの知識がありません...なのに、このレベルなら簡単に試すことができました。
改めて、WebSocketすごいなと(AppSyncもすごいなと)。

GraphQL勉強しておくかー、と思ったので、本日届いた以下の本を読みたいと思います。
出版時期が3、4年前なので少し古いのと、レビューの評価があまり高くなさそうだったので迷ったのですが、GraphQLの良書ってまだ無さそうなんですよね。

https://www.amazon.co.jp/dp/487311893X

Discussion