amplify_flutterでAWS AppSyncのSubscriptionを試す
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にソースを置きました。動画も貼り付けてあります。
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に置いた動画にて確認できます。
5.おわりに
amplify_flutterでAWS AppSyncのSubscriptionを試すことができました。
私はGraphQLの知識がありません...なのに、このレベルなら簡単に試すことができました。
改めて、WebSocketすごいなと(AppSyncもすごいなと)。
GraphQL勉強しておくかー、と思ったので、本日届いた以下の本を読みたいと思います。
出版時期が3、4年前なので少し古いのと、レビューの評価があまり高くなさそうだったので迷ったのですが、GraphQLの良書ってまだ無さそうなんですよね。
Discussion