Hello Devs! In this post, we’ll show how to implement WebSocket communication with STOMP using Spring Boot and Flutter.
· Prerequisites
· Overview
∘ What are WebSockets?
∘ WebSockets Use Cases
∘ WebSocket and STOMP Protocols
· Real-world examples
· Getting Started
∘ Server-side project
∘ Client-side project
· App Results
· Conclusion
· References
Prerequisites
This is the list of all the prerequisites:
- Maven 3.+
- Spring Boot 3+
- Java 17 or later
- Flutter 3+ installed
- Basic knowledge of Dart and Flutter
- Android Studio, Visual Studio Code, or another IDE
- Postman / insomnia or any other API testing tool.
Overview
What are WebSockets?
The WebSocket Protocol (RFC 6455) enables two-way communication between a client running untrusted code in a controlled environment and a remote host that has opted into communications from that code. The security model used for this is the origin-based security model commonly used by web browsers. The protocol consists of an opening handshake followed by basic message framing, layered over TCP. It is designed to work over HTTP, using ports 80 and 443 and allowing re-use of existing firewall rules.
WebSockets Use Cases
WebSockets are used in various scenarios where real-time communication is essential, such as:
- Live Notifications: Real-time alerts and updates, such as stock prices or social media feeds.
- Collaborative Tools: Applications that require multiple users to interact simultaneously.
- IoT applications: Real-time communication between devices and servers in IoT applications.
- Gaming: Multiplayer Online Games.
- Live chat: Real-time messaging between users.
WebSocket and STOMP Protocols
STOMP (Simple Text Oriented Messaging Protocol) was created for scripting languages (such as Ruby, Python, and Perl) to connect to enterprise message brokers. It is designed to address a minimal subset of commonly used messaging patterns. It’s a higher-level protocol that runs on top of the WebSocket protocol, providing a more structured and efficient way of communicating between clients and servers.
- A client connects to a STOMP broker (a server that manages STOMP connections) using the WebSocket protocol.
- The client sends a STOMP frame (a message) to the broker, which includes a command (e.g., SEND, SUBSCRIBE, UNSUBSCRIBE) and a destination (e.g., a topic or queue).
- The broker processes the frame and routes the message to the appropriate destination.
- The broker sends a response frame back to the client, acknowledging receipt of the message.
Real-world examples
We’ll take an airport boarding gate app as an example. This app will be installed on the boarding gate screen and provide passengers with any changes regarding boarding.

The operator sends updated flight change data to the backend and the screen displays real-time gate information with audio sound.
We’ll create a real-time communication system between the client (Flutter App) and the server (Spring Boot).
Getting Started
Server-side project
We will start by creating a simple Spring Boot project from start.spring.io.

Then, let’s enable STOMP over WebSocket.
@Configuration
@EnableWebSocketMessageBroker //(1)
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS(); // (2)
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app") // (3)
.enableSimpleBroker("/topic"); // (4)
}
}
- @EnableWebSocketMessageBroker is the Spring annotation that enables WebSocket message brokering in our server app.
/wsis the HTTP URL for the endpoint to which a SockJS client needs to connect for the WebSocket handshake.- STOMP messages whose destination header begins with
/appare routed to@MessageMappingmethods in@Controllerclasses. - Use the built-in message broker for subscriptions and broadcasting and route messages whose destination header begins with
/topicto the broker.
Implement a controller that will handle operator requests and broadcast received messages to all gate screens subscribed to a given topic by ID.
- GateController
@RestController
@RequestMapping(path = "/gate")
public class GateController {
private final GateService gateService;
public GateController(GateService gateService) {
this.gateService = gateService;
}
@PutMapping("/call")
public ResponseEntity<Void> replaceEmployee(@RequestBody GateInfo gateInfo) {
gateService.updateGateInfo(gateInfo);
return new ResponseEntity<>(HttpStatus.OK);
}
}
- GateService
@RequiredArgsConstructor
@Service
public class GateService {
private final SimpMessagingTemplate simpMessagingTemplate;
public void updateGateInfo(GateInfo gateInfo) {
simpMessagingTemplate.convertAndSend(MessageFormat.format("/topic/gate-info/{0}", gateInfo.getGateId()), gateInfo);
}
}
Client-side project
We’ll start by creating a simple Flutter project.
$ flutter create airport_gate
Let’s first include some external packages that we will need. Add these dependencies to pubspec.yaml file.
dependencies:
flutter_screenutil: ^5.9.1
flutter_bloc: ^8.0.1
stomp_dart_client: ^2.0.0
intl: ^0.19.0
audioplayers: ^6.1.0
We are using:
- flutter_bloc: It makes it easy to separate presentation from business logic, making your code fast, easy to test, and reusable. It is used for state management.
- flutter_screenutil: Adjusts screen and font size.
- stomp_dart_client: It provides an implementation for a STOMP client connecting to a remote server
- intl: It manages date time format.
- audioplayers: It is used to play multiple simultaneous audio files.
Add the logic classes
Now create a bloc class to handle Gate information. It establishes a WebSocket connection using Stomp with SockJS and involves cubit state changes when it receives new data.
class GateInfoCubit extends Cubit<GateInfoState> {
GateInfoCubit() : super(GateInfoInitial());
late StompClient client;
static const baseSocketUrl = "http://localhost:8069/ws"; //backend sockjs endpoint
static const gateId = '66fc36919a4d628f3792a700';
static const audioDir = 'assets/audio';
static String getAudio(audioName) => '$audioDir/$audioName.mp3';
dynamic connect() {
client = StompClient(
config: StompConfig(
useSockJS: true,
url: baseSocketUrl,
beforeConnect: () async {
log('Waiting to connect...');
await Future.delayed(const Duration(milliseconds: 200));
log('Connecting...');
},
onStompError: onStompError,
onWebSocketError: onWebSocketError,
onConnect: onConnect
),
);
client.activate();
}
void onStompError(StompFrame e) {
if (!isClosed) emit(GateInfoError(message: e.body!));
}
void onWebSocketError(dynamic e) {
if (!isClosed) emit(GateInfoError(message: e.body!));
}
void onConnect(StompFrame frame) {
client.subscribe(
destination: '/topic/gate-info/$gateId',
callback: callback,
);
}
void callback(StompFrame event) {
if (event.body != null) {
log('socket body: ${event.body}');
Map<String, dynamic> data = json.decode(event.body!);
playAudioByAssetName('gate');
if (!isClosed) emit(GateInfoSuccess(response: Gate.fromJson(data)));
}
}
Future<void> playAudioByAssetName(audioName) async {
final player = AudioPlayer();
AudioCache.instance = AudioCache(prefix: '');
final audioPath = getAudio(audioName);
await player.play(AssetSource(audioPath));
}
@override
Future<void> close() {
client.deactivate();
return super.close();
}
}
An audio signal is emitted for any new information received.
GateScreen class code source
const marqueeString =
"Passengers should be fully informed of their rights in the event of denied boarding and of cancellation or long delay of flights, so that they can effectively exercise their rights.";
class GateScreen extends StatefulWidget {
const GateScreen({super.key});
@override
State<GateScreen> createState() => _GateScreenState();
}
class _GateScreenState extends State<GateScreen> {
DateTime currentDateTime = DateTime.now();
_getDateTime() {
setState(() {
currentDateTime = DateTime.now();
});
}
@override
void initState() {
super.initState();
BlocProvider.of<GateInfoCubit>(context).connect();
Timer.periodic(const Duration(seconds: 1), (_) => _getDateTime());
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).primaryColor,
body: Container(
margin: const EdgeInsets.only(top: 24, right: 12, left: 12),
padding: const EdgeInsets.only(top: 12, right: 12, left: 12),
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10), topRight: Radius.circular(10)),
color: primaryLightColor,
),
child: BlocBuilder<GateInfoCubit, GateInfoState>(
buildWhen: (previous, current) => previous != current,
builder: (context, state) {
if (state is GateInfoSuccess) {
final info = state.response;
return InfoData(currentDateTime: currentDateTime, gate: info,);
} else {
return NoData(currentDateTime: currentDateTime);
}
},
),
),
);
}
}
class InfoData extends StatelessWidget {
const InfoData({
super.key,
required this.currentDateTime,
required this.gate,
});
final DateTime currentDateTime;
final Gate gate;
@override
Widget build(BuildContext context) {
return Column(
children: [
Align(
alignment: Alignment.topRight,
child: Text(DateFormat('HH:mm:ss').format(currentDateTime),
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: nightBlueColor,
fontWeight: FontWeight.bold,
)),
),
const SizedBox(height: 15.0),
GateDestination(
number: gate.gateNumber,
destination: gate.destination!,
),
const SizedBox(height: 25.0),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Flight No: ${gate.flightNumber}",
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: whiteColor,
)),
Text("Scheduled time: ${DateFormat('HH:mm').format(DateTime.parse(gate.scheduledDate)) }",
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: whiteColor,
)),
],
),
),
const SizedBox(height: 15.0),
Center(
child: Container(
decoration: BoxDecoration(color: getStatusColor(gate.status)),
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 16.0),
child: Text(getStatusTitle(gate.status),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: whiteColor,
)),
),
),
const SizedBox(height: 20.0),
const MarqueeBuilder(
marqueeText: marqueeString,
),
const SizedBox(height: 10.0),
],
);
}
String getStatusTitle(String status){
switch (status) {
case "ON_TIME":
return "On Time";
case "BOARDING":
return "Boarding";
case "DELAYED":
return "Delayed";
case "LAST_CALL":
return "Last call";
default:
return status;
}
}
Color getStatusColor(String status){
switch (status) {
case "ON_TIME":
return Colors.lightBlueAccent;
case "BOARDING":
return Colors.green;
case "DELAYED":
return Colors.orange;
case "LAST_CALL":
return Colors.red;
default:
return Colors.black;
}
}
}
class NoData extends StatelessWidget {
const NoData({
super.key,
required this.currentDateTime,
});
final DateTime currentDateTime;
@override
Widget build(BuildContext context) {
return Column(
children: [
Align(
alignment: Alignment.topRight,
child: Text(DateFormat('HH:mm:ss').format(currentDateTime),
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: nightBlueColor,
fontWeight: FontWeight.bold,
)),
),
const SizedBox(height: 15.0),
Text("Waiting For Data",
style: Theme.of(context).textTheme.displayLarge?.copyWith(
color: Theme.of(context).colorScheme.error,
fontWeight: FontWeight.bold,
fontSize: 100,
)),
const Spacer(),
const MarqueeBuilder(
marqueeText: marqueeString,
),
const SizedBox(height: 10.0),
],
);
}
}
The screen result is as follows when WebSocket receives information:

App Results
As we explained earlier with our example use case, the operator sends an HTTP request with information about the gate which is broadcast on different screens.

Results on the Gate screen
The screens change with each change made by the operator.

Conclusion
Well done !!. This post implements a sample application that involves using Websocket with Spring Boot and Flutter.
The complete source code is available on GitHub.
You can reach out to me and follow me on Medium, Twitter, GitHub, Linkedln
Support me through GitHub Sponsors.
Thank you for Reading !! See you in the next story.