Flutter×Socket.IO チャットアプリをつくる
サーバーサイド
環境構築
わけわからない状態でローカル環境を汚すのは避けたいので、
CodeSandboxを使用してサーバーを構築する。
Socket.IOはNode.js上で動くのでテンプレートはNode HTTP Serverを選択。
立ち上がったら、npm でexpressとsocket.ioをインストールしておく。
serve.js を作成
const crypto = require("crypto");
const app = require("express")();
const http = require("http").createServer(app);
const io = require("socket.io")(http);
// HTMLやJSなどを配置するディレクトリ
const DOCUMENT_ROOT = __dirname + "/public";
/**
* "/"にアクセスがあったらindex.htmlを返却
*/
app.get("/", (req, res) => {
res.sendFile(DOCUMENT_ROOT + "/index.html");
});
app.get("/:file", (req, res) => {
res.sendFile(DOCUMENT_ROOT + "/" + req.params.file);
});
/**
* [イベント] ユーザーが接続
*/
io.on("connection", (socket) => {
console.log("ユーザーが接続しました");
//---------------------------------
// ログイン
//---------------------------------
(() => {
// トークンを作成
const token = makeToken(socket.id);
// 本人にトークンを送付
io.to(socket.id).emit("token", { token: token });
})();
//---------------------------------
// 発言を全員に送信
//---------------------------------
socket.on("post", (msg) => {
io.emit("member-post", msg);
});
});
/**
* 3000番でサーバを起動する
*/
http.listen(3000, () => {
console.log("listening on *:3000");
});
/**
* トークンを作成する
*
* @param {string} id - socket.id
* @return {string}
*/
function makeToken(id) {
const str = "aqwsedrftgyhujiko" + id;
return crypto.createHash("sha1").update(str).digest("hex");
}
引用:はじめてのSocket.io #2 チャット編「自分がemitした通信内容か判定する」 (katsubemakito.net)
返すindex.htmlがないのでコメントアウトしておく。
// app.get("/", (req, res) => {
// res.sendFile(DOCUMENT_ROOT + "/index.html");
// });
設定からpackage.jsonを編集する。
startの項目を node serve.js にしておけば勝手に立ち上がる。
server.jsの解説
ほとんど引用元の転載になってしまうが書く。
クライアントが最初に接続したときに呼ばれるイベント
io.on("connection", (socket) => { // 処理 });
クライアントに送信
io.emit('イベント名', { data:data });
特定のユーザーにだけemitする
io.to(socket.id).emit("イベント名", {data:data});
※Socket.ioではsocket.idから各ユーザーのセッションIDを知ることができる。
クライアントサイド
main.dart
import 'package:flutter/material.dart';
import 'chat.dart';
void main(){
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
const title = 'WebSocket Demo';
return const MaterialApp(
title: title,
home: MyHomePage(
title: title,
),
);
}
}
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 TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Form(
child: TextFormField(
controller: _controller,
decoration: const InputDecoration(labelText: '名前'),
),
),
const SizedBox(height: 24),
ElevatedButton(
child: const Text('次へ'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Chat(name: _controller.text)),
);
},
),
],
),
),
);
}
}
main.dartの解説
次へボタンを押したとき、Chatに名前を渡して遷移させる。
ElevatedButton(
child: const Text('次へ'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Chat(name: _controller.text)),
);
},
),
chat.dart
import 'package:flutter/material.dart';
import 'package:socket_io_client/socket_io_client.dart';
class Chat extends StatefulWidget {
const Chat({
Key? key,
required this.name,
}) : super(key: key);
final String name;
@override
// ignore: library_private_types_in_public_api
_ChatState createState() => _ChatState();
}
//ここからメイン
class _ChatState extends State<Chat> {
final TextEditingController _controller = TextEditingController();
late final Socket _socket;
late EmitData emitData = EmitData();
late List<EmitData> listEmitData = [];
late Color color_text = Colors.black;
@override
void initState() {
super.initState();
_socket = io(
"https://xxxxxx.xxx.codesandbox.io",
OptionBuilder()
.setTransports(['websocket'])
.disableAutoConnect()
.build()
);
_socket.onConnect((data) {
_socket.on("token", (data) {
emitData = EmitData.fromJson(data);
});
});
_socket.on("member-post", (msg) => {
if(mounted){
setState(() {
listEmitData.add(EmitData.fromJson(msg));
})
}
});
// WebSocketに接続
_socket.connect();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("chat"),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Form(
child: TextFormField(
controller: _controller,
decoration: const InputDecoration(labelText: 'メッセージを送る'),
),
),
const SizedBox(height: 24),
ListView.builder(
shrinkWrap: true, //column内のため
itemCount: listEmitData.length,
itemBuilder: (context, index) {
if(listEmitData[index].token == emitData.token){
color_text = Colors.blue;
}else{
color_text = Colors.black;
}
return Text(
"${listEmitData[index].name}> ${listEmitData[index].text}",
style: TextStyle(color: color_text)
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _sendMessage,
tooltip: 'Send message',
child: const Icon(Icons.send),
),
);
}
void _sendMessage() {
emitData.text = _controller.text;
_controller.clear();
_socket.emit("post", {
"text": emitData.text,
"token": emitData.token,
"name": widget.name
});
}
}
class EmitData {
String? text = "";
String token = "token";
String? name = "name";
EmitData();
EmitData.fromJson(Map<String, dynamic> json)
: text = json['text'],
token = json['token'],
name = json['name'];
}
chat.dartの解説
名前(name)を渡すことを要求。requiredつけると渡していないときエラーを返してくれるはず。
const Chat({
Key? key,
required this.name,
}) : super(key: key);
第一引数にサーバーのURL, 第二引数にオプション設定。
今回は自動接続をOFFにしている。(なぜか推奨らしい。)
_socket = io(
"https://xxxxxx.xxx.codesandbox.io",
OptionBuilder()
.setTransports(['websocket'])
.disableAutoConnect()
.build()
);
初回接続時、サーバーからtokenが発行されるので保存する。
これはListView.builder ウィジェットで表示させる。
_socket.onConnect((data) {
_socket.on("token", (data) {
emitData = EmitData.fromJson(data);
});
});
メッセージを受け取ったらリストに追加する。
_socket.on("member-post", (msg) => {
if(mounted){
setState(() {
listEmitData.add(EmitData.fromJson(msg));
})
}
});
socket.ioなので当然だがemitで送信。node.jsと若干書き方が違う点に注意。Dart慣れない…
main.dartから渡された値はwidget.変数名で参照できる。
_socket.emit("post", {
"text": emitData.text,
"token": emitData.token,
"name": widget.name
});
実行結果
リリースビルドしたものを2個立ち上げて実験。
備考
Flutter 3.5.0-12.0.pre.145 • channel master • https://github.com/flutter/flutter.git
Framework • revision a1289a4135 (4 hours ago) • 2022-11-07 06:46:25 -0500
Engine • revision 891d4a3577
Tools • Dart 2.19.0 (build 2.19.0-374.0.dev) • DevTools 2.19.0