4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FlutterでAWS AppSyncのSubscriptionを購読したい

Last updated at Posted at 2022-11-05

この記事はなに?

  • Flutterで
  • AWS AppSyncの
  • Subscriptionを購読する

ことを目的とした記事だよ。

「意図は分かったから、あとはソースコードだけ見せてくれ」という人は、下記リポジトリを参照してね。

用語解説

AWS AppSync

AWSにおける GraphQL + Pub/Sub API サービスだよ。

GraphQL

GraphQLはAPIの問い合わせ言語であり、同時にランタイムでもある存在だよ。端的にいえば、Web-APIをSQLのselect文のように扱える存在で、従来のREST-APIとは色々な面で異なる性質を持っているよ。

Pub/Sub

メッセージを送る『パブリッシャー』と、事前に購読を申し込んで、その都度メッセージを受信する『サブスクライバー』によるメッセージの送受信方式のことだよ。GraphQLにはSubscriptionという機能があって、WebSocketを利用してPub/Sub方式のメッセージ送受信を実現できるよ。GraphQLをサービスとして提供するAWS AppSyncも、このSubscription機能に対応しているよ。

筆者プロフィール

Kenpal株式会社でITエンジニアとして色々いじってる faable01 です。

かつては創作仲間と小説を書いたり、製菓業界で楽しくやっていたはずが、紆余曲折を経て、サーバーレス技術を触るのが好きなITエンジニアになっていました。AWSのIaC兼サーバレス爆速開発ツール 「SST」 が好きです。個人ブログでもたまに記事を書いています。

それから、業務日報SaaS 「RevisNote」 を運営しています。リッチテキストでの日報と、短文SNS感のある分報を書けるのが特徴で、組織に所属する人数での従量課金制です。アカウント開設後すぐ使えて、無料プランもあるから、気軽にお試しください。

環境構築

まずは雛形プロジェクト作成して、ちゃんと起動するか確認するよ。

flutter create flutter_otameshi_appsync_subscription
cd flutter_otameshi_appsync_subscription

# iPhoneのシミュレータを起動してから
flutter run

雛形が動くことを確認できたら、FlutterでAppSyncを利用するために必要な次のふたつのパッケージをインストールしようか。

flutter pub add amplify_flutter
flutter pub add amplify_api

どちらもAWS Amplifyというサービスのためのパッケージだけど、サービスとしてのAmplifyを使わずにAppSyncだけを使いたい場合でも、結局はこのパッケージを利用することになるよ。

今回の記事では取り扱わないけれど、こういう 『サービスとしてのAWS Amplifyは使わないけど、実装側が利用するライブラリとしてはAmplifyパッケージを使いたい』 というパターンは他にもあって、例えばAWSの認可・認証サービスであるところの 『Amazon Cognito』 同じようにAmplifyパッケージを介して利用することがあるよ。

今回の記事でも、最小限の構成で行きたいから、あえて 『サービスとしてのAWS Amplifyは使わずに、AppSync単体で利用する方針』 で進めていくからね。

AWS AppSyncのリソースを作成する

環境構築が終わったら、次は必要なAWSのリソースを作成していこうか。

今回必要となるのは次のふたつのリソースだよ。

  • AWS AppSync(AWSにおける GraphQL + Pub/Sub API のサービス)
  • Amazon DynamoDB(AWSの代表的なNoSQLデータベース。AppSyncのデータソースとして使う)

とはいえ後者のDynamoDBは、今回のお手軽なやり方だとAppSyncのリソース作成時に自動で生成されるから、特に自分で作成する必要はないよ。

そういうわけだから、さっそくAWSマネジメントコンソールから、AppSyncのリソースを作成していこう。

手順1. APIをウィザードで作成

AWSマネジメントコンソールでAppSyncのページに遷移して、メニュー『API』を開くと、画面内に『APIを作成』というボタンがあるはずだから、まずはそれをクリックするよ。

すると次のような画面が開くから『ウィザードで作成』を選んで上にある『開始』ボタンを押してね。

スクリーンショット 2022-11-04 22.17.59.jpg

手順2. モデルを作成

次に進むと今度は『モデルを作成』という見出しの画面が表示される。いくつかフォームが表示されるから、次のように入力するよ。

  • モデル名: otameshi_subscription
  • モデルフィールドを設定: id: ID (必須), message String
  • モデルテーブルを設定(オプション): テーブル名 otameshi_subscriptionTable, プライマリーキー id, ソートキー None

スクリーンショット 2022-11-04 22.23.48.jpg

ここまで入力したら、画面右下のCreateボタンを押す。

手順3. リソースを作成

すると『リソースを作成』という画面になるから、そうしたらAPI名のフォームに otameshi_appsync_subscription と入力して、『作成』ボタンを押そう。

スクリーンショット 2022-11-04 14.54.54.jpg

これでAppSyncのリソースが作成される。作成されたら、どんなスキーマが用意されているかを一応見てみようか。

定義したモデルに合わせて、次のようなスキーマが作成されているはずだよ。長いからこのあと関係する部分だけ抜粋するね。

input CreateOtameshi_subscriptionInput {
	message: String!
}

// ---- 略 ----

type Mutation {
	createOtameshi_subscription(input: CreateOtameshi_subscriptionInput!): otameshi_subscription
    // ---- 略 ----
}

type Query {
    // ---- 略 ----
}

type Subscription {
	onCreateOtameshi_subscription(id: ID, message: String): otameshi_subscription
		@aws_subscribe(mutations: ["createOtameshi_subscription"])
    // ---- 略 ----
}

type otameshi_subscription {
	id: ID!
	message: String!
}

// ---- 略 ----

このあとの解説で関係のあるスキーマはこんなところだね。

手順4. 設定の確認

次は作成したAppSyncのエンドポイントやAPIキーの設定を見てみよう。

サイドバーの設定メニューから開けるからね。

スクリーンショット 2022-11-04 22.35.52.jpg

こんな風に設定画面からAppSyncのエンドポイント(API URL)や、コールするためのAPIキーを確認できるよ。

この後のFlutterの実装で使うから、自分が作成したAppSyncのエンドポイント(API URL)とAPIキーを今のうちにメモしておこうか。

ちなみに、上記キャプチャのエンドポイントやAPIキーは、この記事を書き終えた頃にはもうリソースごと消えているはずだから、使っても特に意味はないからね。

メモしたら、AWSマネジメントコンソールでやるべきことはここまで。次は早速Flutterの実装に進むよ。

Flutter実装

lib/amplifyconfiguration.dart

まずはAppSyncの設定をベースに、Amplifyパッケージを利用するための設定ファイル lib/amplifyconfiguration.dart を作成しよう。

lib/amplifyconfiguration.dart
const amplifyconfig = '''{
  "api": {
    "plugins": {
      "awsAPIPlugin": {
        "otameshi_appsync_subscription": {
          "endpointType": "GraphQL",
          "endpoint": "[AppSyncのエンドポイント]",
          "region": "[AppSyncのリージョン]",
          "authorizationType": "API_KEY",
          "apiKey": "[AppSyncのAPIキー]"
        }
      }
    }
  }
}''';

AppSyncのエンドポイント と、 AppSyncのAPIキー はさっき設定画面でメモしてくれた値を入力するといいよ。

それから、AppSyncのリージョン は、東京リージョンなら ap-northeast-1 だよ。自分が作成したAppSyncのリージョンが分からないという人は、AWSマネジメントコンソールの画面右上のリージョン選択の表示から、自分が作成したリソースがどのリージョンにあるものなのかを確認してね。

lib/main.dart

lib/amplifyconfiguration.dart を実装したら、次はmain.dartを書くよ。AppSyncのスキーマに定義されたサブスクリプション onCreateOtameshi_subscription を購読して、誰かが createOtameshi_subscription を実行するたびに、その結果を随時受信するようコードを実装していくね。

順を追って解説してもいいけど、かえって分かりにくいだろうから、あえてmain.dartの全文をずらっと書き出しちゃうよ。

main.dart
import 'dart:async';

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

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

// 上記でサブスクリプションするアプリを作る
class _MyAppState extends State<MyApp> {
  /// サブスクリプションの結果を受け取るためのStream
  StreamSubscription<GraphQLResponse<String>>? subscription = null;

  /// 受信したJSONを文字列のリストで管理するstate
  List<String> _receivedJsons = [];

  /// Amplifyの初期化を行うメソッド
  void _configureAmplify() async {
    try {
      Amplify.addPlugins([AmplifyAPI()]);
      await Amplify.configure(amplifyconfig);
      print('Amplify configured');
    } catch (e) {
      print('Error occurred while configuring Amplify: $e');
    }
  }

  /// サブスクリプション購読を開始するメソッド
  void _startSubscribe() async {
    const graphQLDocument = '''subscription otameshi_subscription {
      onCreateOtameshi_subscription {
        id
        message
      }
    }''';
    final Stream<GraphQLResponse<String>> operation = Amplify.API.subscribe(
      GraphQLRequest<String>(document: graphQLDocument),
      onEstablished: () => print('Subscription established'),
    );
    setState(() {
      subscription = operation.listen((event) {
        print('Subscription event data received: ${event.data}');
        setState(() {
          // event.dataに現在日時のラベルを [YYYY-MM-DD hh:mm:ss] で追加する
          _receivedJsons.add(
              '[${DateTime.now().toLocal().toString().split('.')[0]}] ${event.data}');
        });
      }, onError: (e) {
        print('Error in subscription stream: $e');
      });
    });
  }

  /// サブスクリプション購読を終了するメソッド
  void _stopSubscribe() {
    setState(() {
      subscription?.cancel();
      subscription = null;
      _receivedJsons = [];
    });
  }

  /// 初期化時にAmplifyの設定を行う
  @override
  void initState() {
    super.initState();
    _configureAmplify();
  }

  /// 画面のフッター近辺にサブスクリプション購読開始/終了ボタンを配置する
  /// また、画面中央には枠線で囲まれたスクロール可能エリアを配置し、
  /// その中にサブスクリプションで受信したJSONを表示する
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            Expanded(
              child: Container(
                margin: const EdgeInsets.fromLTRB(
                  30,
                  60,
                  30,
                  30,
                ),
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.black),
                ),
                child: Scrollbar(
                  child: ListView.separated(
                    itemCount: _receivedJsons.length,
                    itemBuilder: (context, index) {
                      return Container(
                        margin: const EdgeInsets.all(20),
                        child: Text(_receivedJsons[index]),
                      );
                    },
                    separatorBuilder: (context, index) {
                      return Container(
                        height: 1,
                        color: Colors.grey[500],
                      );
                    },
                  ),
                ),
              ),
            ),
          ],
        ),
        bottomNavigationBar: BottomAppBar(
          child: Container(
            width: double.infinity,
            height: 80,
            child: TextButton(
              onPressed: () {
                if (subscription == null) {
                  _startSubscribe();
                } else {
                  _stopSubscribe();
                }
              },
              style: ButtonStyle(
                backgroundColor: MaterialStateProperty.all<Color>(
                  subscription == null ? Colors.green : Colors.red,
                ),
              ),
              child: Text(
                subscription == null ? 'サブスクリプションを開始する' : 'サブスクリプションを停止する',
                style: TextStyle(
                  color: subscription == null ? Colors.black : Colors.white,
                  fontSize: 20,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

void main() {
  runApp(MyApp());
}

こんな感じだね。ちなみに筆者はFlutterの状態管理をStatefulWidgetしか知らないから、上記のように実装しているけど、もっと今時の優れた状態管理を知ってる人は、自分の好きな状態管理アプローチに置き換えてコードを読む or 実装してね。

さて、全体としてはこの通りだけど、ただ全体像を提示しただけでは分かりにくいと思うから、重要な部分をピックアップするね。

Amplifyパッケージ利用のための設定処理

Amplifyパッケージを利用するために lib/amplifyconfiguration.dart を実装してもらったけど、これを実際にアプリケーションに組み込むための設定処理は、次の通りだよ。

main.dartのうち設定処理に関するもの
import 'amplifyconfiguration.dart';

// ---- 略 ----

class _MyAppState extends State<MyApp> {

  // ---- 略 ----

  void _configureAmplify() async {
    try {
      Amplify.addPlugins([AmplifyAPI()]);
      await Amplify.configure(amplifyconfig);
      print('Amplify configured');
    } catch (e) {
      print('Error occurred while configuring Amplify: $e');
    }
  }

  // ---- 略 ----

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

  // ---- 略 ----
}

initStateのタイミングで lib/amplifyconfiguration.dart をAmplify.configureの引数に渡して設定する処理を実行しているんだね。

AppSyncのSubscriptionの購読を開始・終了するコード

Subscriptionは引数の指定で、「特定の条件に一致するレコードだけを受信する」ことが可能だけど、今回は単にAppSyncのSubscription購読がちゃんと動くかを確かめたいだけだから、引数は指定なしのまま全レコードを購読するようにしているからね。

さて、さっそくSubscriptionの購読開始・終了処理の実装を見ていくよ。

main.dartにおけるSubscription購読処理
class _MyAppState extends State<MyApp> {
  /// サブスクリプションの結果を受け取るためのStream
  StreamSubscription<GraphQLResponse<String>>? subscription = null;

  // ---- 略 ----

  /// サブスクリプション購読を開始するメソッド
  void _startSubscribe() async {
    const graphQLDocument = '''subscription otameshi_subscription {
      onCreateOtameshi_subscription {
        id
        message
      }
    }''';
    final Stream<GraphQLResponse<String>> operation = Amplify.API.subscribe(
      GraphQLRequest<String>(document: graphQLDocument),
      onEstablished: () => print('Subscription established'),
    );
    setState(() {
      subscription = operation.listen((event) {
        print('Subscription event data received: ${event.data}');
        setState(() {
          // event.dataに現在日時のラベルを [YYYY-MM-DD hh:mm:ss] で追加する
          _receivedJsons.add(
              '[${DateTime.now().toLocal().toString().split('.')[0]}] ${event.data}');
        });
      }, onError: (e) {
        print('Error in subscription stream: $e');
      });
    });
  }

  /// サブスクリプション購読を終了するメソッド
  void _stopSubscribe() {
    setState(() {
      subscription?.cancel();
      subscription = null;
      _receivedJsons = [];
    });
  }

こんな感じだね。GraphQLのクエリを文字列で定義して、Amplify.API.subscribeメソッドの返り値をlistenすることで、Subscriptionを購読しているんだ。購読を終えるときは、同じくAmplify.API.subscribeメソッドの返り値を元にcancelメソッドを実行すればいいよ。

ちなみにSubscriptionの購読にはもう一つ方法があって、ざっくり紹介するとこんな風にfor文をbreakするまで自動で購読し続けさせることができるよ( https://pub.dev/packages/amplify_api より引用)

Subscriptionの購読方法その2(今回は採用しなかった。クエリや処理内容はサンプルのままであり、ここまでに実装してきた処理とは別物です)
Future<void> subscribe() async {
  final graphQLDocument = '''subscription MySubscription {
    onCreateBlog {
      id
      name
      createdAt
    }
  }''';
  final Stream<GraphQLResponse<String>> operation = Amplify.API.subscribe(
    GraphQLRequest<String>(document: graphQLDocument),
    onEstablished: () => print('Subscription established'),
  );

  try {
    // Retrieve 5 events from the subscription
    var i = 0;
    await for (var event in operation) {
      i++;
      print('Subscription event data received: ${event.data}');
      if (i == 5) {
        break;
      }
    }
  } on Exception catch (e) {
    print('Error in subscription stream: $e');
  }
}

2つの異なるやり方でSubscriptionの購読ができることは、頭の片隅に入れておくといいよ。

実装はここまで! 書き終えたら、さっそく flutter run で動かしてみよう。

アプリが起動したら、画面最下部に『サブスクリプションを開始する』というボタンがあるから、それを押せば購読が始まるよ。

購読しているSubscriptionは、誰かが Mutation createOtameshi_subscription を実行するたびに、その実行結果を受信してくれるはず。

試してみるには、AWSマネジメントコンソールからAppSyncの『クエリ』画面が便利だよ。実際にクエリ画面から createOtameshi_subscription を実行してやると……次のGIFのように、実行のたびに購読中のアプリがちゃんとメッセージを受信できているのが分かるよね。

subscriptioin_sample.gif

そういうわけで、これで 『FlutterでAWS AppSyncのSubscriptionを購読する』 という目的は達成だね。

まとめ

FlutterでAWS AppSyncのSubscriptionを購読するには、次の2つのパッケージを使うよ。

いずれもAmplifyというAWSのサービスのパッケージだけど、サービスとしてのAmplifyを使わない状態でも、これらのパッケージを使ってAppSyncを利用できる。

また、実装時にはまず Amplify.configure(amplifyconfig); でAPIキーやエンドポイントの設定を行ない、その後、サブスクリプションの購読を開始・終了するコードを実行させるよ。

その際、サブスクリプションの購読にはふたつの方法がある(listenパターン、for文パターン)ので、適宜好きなものを選んで実装してね。

なお、この記事で解説したコードは、下記のリポジトリで公開しているよ。コードをあらためて確認したくなった時などに参照してね。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?