この記事はなに?
- 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を作成』というボタンがあるはずだから、まずはそれをクリックするよ。
すると次のような画面が開くから『ウィザードで作成』を選んで上にある『開始』ボタンを押してね。
手順2. モデルを作成
次に進むと今度は『モデルを作成』という見出しの画面が表示される。いくつかフォームが表示されるから、次のように入力するよ。
- モデル名:
otameshi_subscription
- モデルフィールドを設定:
id: ID (必須)
,message String
- モデルテーブルを設定(オプション): テーブル名
otameshi_subscriptionTable
, プライマリーキーid
, ソートキーNone
ここまで入力したら、画面右下のCreateボタンを押す。
手順3. リソースを作成
すると『リソースを作成』という画面になるから、そうしたらAPI名のフォームに otameshi_appsync_subscription
と入力して、『作成』ボタンを押そう。
これで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キーの設定を見てみよう。
サイドバーの設定メニューから開けるからね。
こんな風に設定画面からAppSyncのエンドポイント(API URL)や、コールするためのAPIキーを確認できるよ。
この後のFlutterの実装で使うから、自分が作成したAppSyncのエンドポイント(API URL)とAPIキーを今のうちにメモしておこうか。
ちなみに、上記キャプチャのエンドポイントやAPIキーは、この記事を書き終えた頃にはもうリソースごと消えているはずだから、使っても特に意味はないからね。
メモしたら、AWSマネジメントコンソールでやるべきことはここまで。次は早速Flutterの実装に進むよ。
Flutter実装
lib/amplifyconfiguration.dart
まずはAppSyncの設定をベースに、Amplifyパッケージを利用するための設定ファイル 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の全文をずらっと書き出しちゃうよ。
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
を実装してもらったけど、これを実際にアプリケーションに組み込むための設定処理は、次の通りだよ。
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の購読開始・終了処理の実装を見ていくよ。
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 より引用)
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のように、実行のたびに購読中のアプリがちゃんとメッセージを受信できているのが分かるよね。
そういうわけで、これで 『FlutterでAWS AppSyncのSubscriptionを購読する』 という目的は達成だね。
まとめ
FlutterでAWS AppSyncのSubscriptionを購読するには、次の2つのパッケージを使うよ。
いずれもAmplifyというAWSのサービスのパッケージだけど、サービスとしてのAmplifyを使わない状態でも、これらのパッケージを使ってAppSyncを利用できる。
また、実装時にはまず Amplify.configure(amplifyconfig);
でAPIキーやエンドポイントの設定を行ない、その後、サブスクリプションの購読を開始・終了するコードを実行させるよ。
その際、サブスクリプションの購読にはふたつの方法がある(listenパターン、for文パターン)ので、適宜好きなものを選んで実装してね。
なお、この記事で解説したコードは、下記のリポジトリで公開しているよ。コードをあらためて確認したくなった時などに参照してね。