成果物
Serverpodでチャットアプリを作り、ブラウザとPCアプリでチャットすることができました。
はじめに
Serverpodという開発フレームワークをご存知でしょうか?
Dartという共通言語でサーバからフロントまで構築でき、かつ、マルチプラットフォームに開発できるものです。
意外と流行っていない印象がありますが、面白そうなので使ってみました。
今回はマルチプラットフォームな簡易チャットアプリを作りました。
開発環境
OS: Ubuntu 22.04 LTS
Flutter: 3.10.4
Dart: 3.0.3
構築
serverpodのインストール
まだの方は、serverpodをインストールしてください。
exampleアプリの構築
まずは、serverpod createコマンドでアプリを作ります。
$ serverpod create chatapp
$ cd chatapp/chatapp_server
$ docker-compose up --build --detach
これで、バックエンドも含めて、exampleのアプリが立ち上がるのがびっくりです。
今回は分かりやすいように、このexampleのアプリを改修していきます。
websocket用の型の定義
chatapp/chatapp_server/lib/src/protocol内に、以下のchat.yamlを生成します。
class: Chat
table: chat
fields:
message: String
user_id: int
上記ファイル生成後、serverpod generateして、classファイルを生成します。
$ serverpod generate
chatapp/chatapp_server/generated/tables.pgsqlにデータベースのテーブル作成用のSQL文が自動生成されています。serverpodのチュートリアル動画では、ここでデータベースにログインして生成していますが、今回は省略します。
--
-- Class Chat as table chat
--
CREATE TABLE "chat" (
"id" serial,
"message" text NOT NULL,
"user_id" integer NOT NULL
);
ALTER TABLE ONLY "chat"
ADD CONSTRAINT chat_pkey PRIMARY KEY (id);
エンドポイント処理の定義
chatapp/chatapp_server/lib/src/endpoint内に、エンドポイント処理定義用のコードファイル,chat_endpoint.dartを生成します。
import 'dart:async';
import 'package:serverpod/serverpod.dart';
import '../generated/protocol.dart'; //NOTE Chat型を使用するために追加
class ChatEndpoint extends Endpoint {
final String _hello = "hello";
@override
Future<void> streamOpened(StreamingSession session) async {
//NOTE clientからのwebsocket接続要求時の処理
print("chat websocket started");
session.messages.addListener(_hello, (message) {
//リスナーの追加
print("send to client");
sendStreamMessage(session, message);
});
}
@override
Future<void> handleStreamMessage(//NOTE クライアントからwebsocketを受信したときの処理。
StreamingSession session,
SerializableEntity message,
) async {
print("websocket handle");
if (message is Chat) {//NOTE Chatメッセージが来た時の処理
print("recieve Chat");
session.messages.postMessage(
_hello, Chat(message: message.message, user_id: message.user_id));
}
}
@override
Future<void> streamClosed(StreamingSession session) async {
print("stream close");
}
}
上記ファイル生成後、もう一度serverpod generateして、client用のファイルを生成します。
$ serverpod generate
すると、chatapp/chatapp_clientの中にclient用のコードが自動生成されます。
このコードを、flutterから使用します。
フロントエンド
最後にflutterのコードを修正します。
chatapp/chatapp_flutter/lib/main.dartを以下のように編集します。
import 'package:marubatsugame_client/marubatsugame_client.dart';
import 'package:flutter/material.dart';
import 'package:serverpod_flutter/serverpod_flutter.dart';
import 'dart:math' as math;
var client = Client('http://localhost:8080/')
..connectivityMonitor = FlutterConnectivityMonitor();
final int _userId = math.Random().nextInt(1000); //ランダムなユーザIDを生成
Future<void> main() async {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Serverpod Chat Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Serverpod Chat Example'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
MyHomePageState createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
final List<Chat> _messages = [];
@override
void initState() {
Future(() async {
await client.openStreamingConnection();
await for (var message in client.chat.stream) {
if (message is Chat) {
_messages.add(message); //NOTE メッセージを追加
}
setState(() {});
}
});
super.initState();
}
@override
void dispose() {
client.closeStreamingConnection(); //NOTE websocketコネクションの開放
super.dispose();
}
final _textEditingController = TextEditingController();
void _sendMessage() async {
try {
//NOTE APIではなく、websocketに変更
Chat message =
Chat(message: _textEditingController.text, user_id: _userId);
await client.chat
.sendStreamMessage(message);
} catch (e) {
print(e);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: TextField(
controller: _textEditingController,
decoration: const InputDecoration(
hintText: 'Enter chat Message', //NOTE ヒントテキストを変更
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: ElevatedButton(
onPressed: _sendMessage,
child: const Text('Send to Server'),
),
),
for (final message in _messages)//NOTE メッセージの表示
_ChatDisplay(message: message.message, userId: message.user_id)
],
),
),
);
}
}
class _ChatDisplay extends StatelessWidget {
//NOTE chat用のwidgetを生成
final String? message;
final int? userId;
const _ChatDisplay({
required this.message,
required this.userId,
});
@override
Widget build(BuildContext context) {
return Container(
height: 50,
color: userId == _userId
? Colors.grey[300]
: Colors.green[300], //NOTE 自分と自分以外で、色分け
child: Center(
child: Text(message!),
),
);
}
}
以上で、コードの編集は終わりです。
起 動
バックエンドを起動します。
$ cd chatapp_server
$ dart bin/main.dart
フロントエンド(Flutter)を起動します。
マルチプラットフォームをアピールするため、web版(chrome)とPCアプリ(linux)の2つ、起動します。
$ flutter run -d chrome
別ターミナルで、
$ flutter run -d linux
実際の様子(再掲)
web版とPCアプリ版で、websocketを用いて、相互通信することができました。
処理の流れ
- ブラウザ(chrome)からサーバにwebsocketでメッセージ送信
- サーバから各websocket client(chrome, linux)にメッセージをブロードキャスト
- 各websocket clientは受け取ったメッセージを描画
おわりに
serverpodを使用し、マルチプラットフォームな簡易チャットアプリを作成しました。
バックエンドからフロントエンドまでのシームレスな開発は癖になります。
今後はデータベースも絡めた簡単な対戦ゲームを作っていこうと思います。
参考資料
Tutorials & Examples
https://docs.serverpod.dev/tutorials
このチュートリアルを見れば体系的に学べます。
特にserverpod generateコマンドで、何をやっているかがだいたい分かります。
Streams and messaging
https://docs.serverpod.dev/concepts/streams
今回の肝である、websocketの公式ガイドです。