การใช้ WebSockets ใน Flutter: สร้าง Realtime Communication กับ Server
ในยุคปัจจุบันที่ข้อมูลเปลี่ยนแปลงอยู่ตลอดเวลา การสร้างแอปพลิเคชันที่สามารถสื่อสารแบบ Real-time ได้กลายเป็นสิ่งสำคัญอย่างยิ่ง WebSockets คือเทคโนโลยีที่ตอบโจทย์นี้ได้เป็นอย่างดี โดยเปิดช่องทางการสื่อสารแบบสองทาง (bidirectional) ระหว่าง client และ server ทำให้ข้อมูลสามารถถูกส่งไปมาได้ทันทีโดยไม่ต้องมีการร้องขอซ้ำๆ เหมือน HTTP polling
บทความนี้จะพาคุณไปทำความรู้จักกับการใช้งาน WebSockets ใน Flutter เพื่อสร้างแอปพลิเคชันที่สามารถรับส่งข้อมูลแบบ Real-time ได้อย่างมีประสิทธิภาพ โดยเราจะเน้นไปที่การใช้งาน WebSocket client และการจัดการข้อมูลผ่าน Streams
ทำไมต้อง WebSockets?
ก่อนที่จะลงมือเขียนโค้ด เรามาทำความเข้าใจกันก่อนว่าทำไม WebSockets ถึงมีความสำคัญและแตกต่างจาก HTTP polling อย่างไร:
- Real-time Communication: WebSockets ช่วยให้ข้อมูลถูกส่งไปมาได้ทันทีโดยไม่ต้องรอการร้องขอจาก client ทำให้แอปพลิเคชันสามารถตอบสนองต่อการเปลี่ยนแปลงของข้อมูลได้อย่างรวดเร็ว
- Bidirectional Communication: WebSockets เปิดช่องทางการสื่อสารแบบสองทาง ทำให้ทั้ง client และ server สามารถส่งข้อมูลให้กันและกันได้ตลอดเวลา
- Reduced Latency: เนื่องจากไม่มี overhead จากการสร้าง connection ใหม่ทุกครั้งเหมือน HTTP polling, WebSockets จึงมี latency ที่ต่ำกว่ามาก
- Efficiency: WebSockets ใช้ resource น้อยกว่า HTTP polling เพราะไม่ต้องมีการสร้าง connection ใหม่ทุกครั้ง
WebSockets เหมาะสำหรับแอปพลิเคชันที่ต้องการการอัปเดตข้อมูลแบบ Real-time เช่น:
- แอปพลิเคชันแชท
- แอปพลิเคชันติดตามหุ้น
- เกมออนไลน์
- แอปพลิเคชัน IoT
WebSocket Client ใน Flutter
Flutter มี package ที่ชื่อว่า web_socket_channel ที่ช่วยให้เราสามารถใช้งาน WebSockets ได้อย่างง่ายดาย ในการเริ่มต้นใช้งาน ให้เพิ่ม dependency นี้ลงใน pubspec.yaml:
dependencies:
flutter:
sdk: flutter
web_socket_channel: ^2.0.0
หลังจากนั้น ให้ทำการ flutter pub get เพื่อติดตั้ง package
ต่อไป เราจะสร้าง WebSocket client เพื่อเชื่อมต่อกับ WebSocket server:
import 'package:flutter/material.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WebSocket Demo',
home: MyHomePage(
title: 'WebSocket Demo Home Page',
channel: WebSocketChannel.connect(
Uri.parse('wss://echo.websocket.events'), // Replace with your WebSocket server URL
),
),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
final WebSocketChannel channel;
MyHomePage({Key? key, required this.title, required this.channel}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
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: InputDecoration(labelText: 'Send a message:'),
),
),
SizedBox(height: 24),
StreamBuilder(
stream: widget.channel.stream,
builder: (context, snapshot) {
return Text(snapshot.hasData ? '${snapshot.data}' : '');
},
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _sendMessage,
tooltip: 'Send message',
child: Icon(Icons.send),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
void _sendMessage() {
if (_controller.text.isNotEmpty) {
widget.channel.sink.add(_controller.text);
_controller.clear();
}
}
@override
void dispose() {
widget.channel.sink.close();
super.dispose();
}
}
โค้ดนี้สร้างแอปพลิเคชันง่ายๆ ที่เชื่อมต่อกับ WebSocket server ที่ wss://echo.websocket.events (ซึ่งเป็น server ที่ใช้สำหรับทดสอบ WebSocket) และแสดงข้อความที่ได้รับจาก server ใน StreamBuilder
คำอธิบายโค้ด:
WebSocketChannel.connect(Uri.parse('wss://echo.websocket.events')): สร้าง WebSocket channel เพื่อเชื่อมต่อกับ serverwidget.channel.stream: เป็น Stream ที่รับข้อมูลจาก WebSocket serverwidget.channel.sink.add(_controller.text): ส่งข้อความไปยัง WebSocket serverwidget.channel.sink.close(): ปิด WebSocket connection เมื่อ Widget ถูก disposeStreamBuilder: Widget ที่ใช้สำหรับรับข้อมูลจาก Stream และแสดงผล
การจัดการ State และ Streams อย่างมีประสิทธิภาพ
ในตัวอย่างข้างต้น เราใช้ StreamBuilder เพื่อรับข้อมูลจาก WebSocket stream โดยตรง แต่ในแอปพลิเคชันที่ซับซ้อนมากขึ้น การจัดการ state และ streams อย่างมีประสิทธิภาพเป็นสิ่งสำคัญ เราสามารถใช้ Provider หรือ BLoC pattern เพื่อจัดการ state และ streams ได้อย่างมีโครงสร้าง
ตัวอย่างการใช้ Provider:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => WebSocketProvider(
WebSocketChannel.connect(
Uri.parse('wss://echo.websocket.events'),
),
),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WebSocket Demo',
home: MyHomePage(title: 'WebSocket Demo Home Page'),
);
}
}
class WebSocketProvider extends ChangeNotifier {
final WebSocketChannel channel;
String _message = '';
WebSocketProvider(this.channel) {
channel.stream.listen((data) {
_message = data;
notifyListeners();
});
}
String get message => _message;
void sendMessage(String message) {
channel.sink.add(message);
}
@override
void dispose() {
channel.sink.close();
super.dispose();
}
}
class MyHomePage extends StatelessWidget {
final String title;
MyHomePage({Key? key, required this.title}) : super(key: key);
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
final webSocketProvider = Provider.of<WebSocketProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Form(
child: TextFormField(
controller: _controller,
decoration: InputDecoration(labelText: 'Send a message:'),
),
),
SizedBox(height: 24),
Text(webSocketProvider.message),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
webSocketProvider.sendMessage(_controller.text);
_controller.clear();
},
tooltip: 'Send message',
child: Icon(Icons.send),
),
);
}
}
ในตัวอย่างนี้ เราสร้าง WebSocketProvider ที่จัดการ WebSocket channel และ state ของข้อความที่ได้รับ เมื่อมีข้อความใหม่เข้ามา _message จะถูกอัปเดตและ notifyListeners() จะถูกเรียกเพื่อแจ้งให้ Widget ที่ listen อยู่ทราบถึงการเปลี่ยนแปลง
เหตุผลเชิงออกแบบ: การใช้ Provider ช่วยให้เราแยก logic ของ WebSocket ออกจาก UI ทำให้โค้ดมีความสะอาดและ maintainable มากขึ้น นอกจากนี้ Provider ยังช่วยให้เราสามารถเข้าถึง WebSocket channel และ state ได้จากทุกที่ใน Widget tree
ข้อควรระวังในการใช้งาน WebSockets
ถึงแม้ว่า WebSockets จะมีประโยชน์อย่างมาก แต่ก็มีข้อควรระวังบางประการที่ต้องคำนึงถึง:
- Error Handling: ควรมีการจัดการ error ที่อาจเกิดขึ้นระหว่างการเชื่อมต่อ WebSocket เช่น connection timeout หรือ server error
- Reconnection: ในกรณีที่ connection ถูกตัดขาด ควรมีการ implement logic สำหรับการ reconnect อัตโนมัติ
- Security: ควรใช้ WSS (WebSocket Secure) แทน WS เพื่อเข้ารหัสข้อมูลที่ส่งผ่าน WebSocket
- Scalability: เมื่อมีจำนวน client เพิ่มขึ้น ควรมีการออกแบบ server ให้สามารถรองรับ load ได้อย่างมีประสิทธิภาพ
สรุป
WebSockets เป็นเครื่องมือที่มีประสิทธิภาพในการสร้างแอปพลิเคชันที่ต้องการการสื่อสารแบบ Real-time ใน Flutter เราสามารถใช้ package web_socket_channel เพื่อเชื่อมต่อกับ WebSocket server และใช้ Streams เพื่อจัดการข้อมูลที่ได้รับ การจัดการ state และ streams อย่างมีประสิทธิภาพด้วย Provider หรือ BLoC pattern จะช่วยให้โค้ดมีความสะอาดและ maintainable มากขึ้น
ในบทความถัดไป เราจะไปเรียนรู้เกี่ยวกับการใช้ Isolate ใน Flutter เพื่อให้แอปพลิเคชันสามารถทำงาน background task ได้โดยไม่กระทบต่อ UI thread