Welcome back, intrepid tech explorers! If you survived the roller-coaster of gRPC, Flutter, and Golang from our last adventure, congratulations! You're now ready for Part 2: "Wtf is gRPC? Part 2: Custom Notification (without Firebase) in Flutter and Golang." 🎉
In this next thrilling installment, we'll tackle custom notifications without the Firebase safety net. Get ready to dive into the world of Flutter and Golang, where we'll craft notifications that are as unique as your favorite coding quirks. So, fasten your seatbelts and keep your code close; it's going to be a wild ride through the land of tech enchantment! 🚀💻✨
Table of Contents: Navigating the Streaming Circus 🎪
- The Streaming Showdown: gRPC Unleashed
- Let's Play with Data: The Streaming Playground
- Setting Up the Golang Server: Where Code Meets Magic
- Fluttering into Action: Your Magical Streaming App
The Streaming Showdown: gRPC Unleashed
In simple words A server-streaming RPC is similar to a unary RPC, except that the server returns a stream of messages in response to a client’s request. After sending all its messages, the server’s status details (status code and optional status message) and optional trailing metadata are sent to the client. This completes processing on the server side. The client completes once it has all the server’s messages.
Unary vs. Server Streaming: The Tech Battle of the Century
In this epic tech showdown, I am going to compare Unary Streaming and Server Streaming, with a touch of TV series for better understanding.
Aspect | Unary Streaming | Server Streaming | TV Series Comparison |
---|---|---|---|
Data Transfer | Single requests | Continuous data stream | The Office (Episode-by-episode) |
Communication Overhead | Less | Slightly more | Friends (Constant chatter) |
Real-time Updates | Not suitable | Ideal for real-time | Stranger Things (Always suspenseful) |
Complexity | Simpler | More complex | Black Mirror (Mind-bending) |
Scalability | Limited by requests | Highly scalable | Game of Thrones (Vast world) |
Use Cases | Simple operations | Real-time data streaming | Breaking Bad (Intense) |
Resource Consumption | Lower | Higher | The Mandalorian (High production) |
Parallel Processing | Limited | Effective | The Big Bang Theory (Many characters) |
Error Handling | Simple | Handling errors in streams | The Twilight Zone (Mysterious) |
Performance | Suitable for small-scale | Suitable for large-scale | The Walking Dead (Epic) |
Winner | Suitable for specific tasks | Ideal for real-time data | Server Streaming Showdown! |
Let's Play with Data: The Streaming Playground
Let's play with streaming! Imagine that the client is a stand-up comedian whose job is to tell jokes, and the server, representing the audience, responds with laughter until they've had enough."
- Let's create required message and service.
joke.proto
syntax = "proto3";
package pb;
option go_package="github.com/djsmk123/server/pb";
//Response send by standup comedian to audience
message Joke{
string joke=1;
}
// Response Recieved by standup comedian from audience
message JokeResponse{
string laugh_intensity=1;
};
rpc_services.proto
:
syntax="proto3";
package pb;
option go_package="github.com/djsmk123/server/pb";
import "joke.proto";
service GrpcServerService {
rpc ThrowAJoke(Joke) returns(stream JokeResponse){};
}
- Implement
ThrowAJoke()
function in golang.
func (s *Server) ThrowAJoke(joke *pb.Joke, stream pb.GrpcServerService_ThrowAJokeServer) error {
for i := 1; i <= 10; i++ {
response := &pb.JokeResponse{
LaughIntensity: fmt.Sprintf("LOL %d", i),
}
if err := stream.Send(response); err != nil {
return err
}
time.Sleep(1 * time.Second)
}
return nil
}
- Let's call service now.
Setting Up the Golang Server: Where Code Meets Magic
We are going to create a notification service that will listen to a notification-gRPC streaming service in the background. If any new user is added to the MongoDB collection named users, we will send data to the listener, which will then display notifications in the application.
- Let's continue code from part 1,where we left.
git clone git@github.com:Djsmk123/Wtf-is-grpc.git
- Create a new message for the notification response that has to be returned by the server.
rpc_notification.proto
syntax="proto3";
package pb;
option go_package="github.com/djsmk123/server/pb";
message NotificationMessage{
int32 id=1;
string title=2;
string description=3;
}
- Create new
rpc
inrpc_services.proto
. ```
//keep everything same
//import NotificationMessage
import "rpc_notifications.proto";
//same
service GrpcServerService {
//keep same
// create new rpc function here
rpc GetNotifications(EmptyRequest) returns (stream NotificationMessage){}
}
- Create equivalent code for golang from proto.
protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative \
--go-grpc_out=pb --go-grpc_opt=paths=source_relative \
proto/*.proto
- Create new package called `services` in root folder(`server`).
> We are using **goroutines** and **channels** to listen for changes in the database. If you want to learn more about goroutines, you can read this [blog](https://betterprogramming.pub/golang-how-to-implement-concurrency-with-goroutines-channels-2b78b8077984)
- Create new services `notification.go`.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package services
import (
"context"
"fmt"
"log"
"math/rand"
"time"
"github.com/djsmk123/server/db/model"
"github.com/djsmk123/server/pb"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
// NotificationNewUser is a function that listens for new user additions in a MongoDB collection and sends notifications.
func NotificationNewUser(collection *mongo.Collection, notificationCh chan *pb.NotificationMessage) {
// Create a context with a timeout
ctx := context.Background()
//defer cancel() // Deferred cancel() call, if needed.
fmt.Println("Notification service called")
// Define a pipeline to watch for changes (insertions)
pipeline := []bson.M{
{
"$match": bson.M{
"operationType": "insert", // Watch for insertions
"fullDocument.name": bson.M{
"$exists": true, // Ensure the 'name' field exists
},
},
},
}
// Create a change stream
changeStream, err := collection.Watch(ctx, pipeline)
if err != nil {
fmt.Println("Error creating change stream error:", err)
return
}
fmt.Println("Listening for new user additions...")
// Start a goroutine to handle changes from the stream
for changeStream.Next(ctx) {
var changeDocument bson.M
if err := changeStream.Decode(&changeDocument); err != nil {
log.Println("Error decoding change document:", err)
continue
}
fullDocumentJSON, err := bson.MarshalExtJSON(changeDocument["fullDocument"], false, false)
if err != nil {
log.Println("Error converting fullDocument to JSON:", err)
continue
}
// Extract the updated user name from the change document
var user model.UserModel
fmt.Println("user:", string(fullDocumentJSON))
if err := bson.UnmarshalExtJSON(fullDocumentJSON, false, &user); err != nil {
log.Println("Error unmarshaling user:", err)
continue
}
rand.Seed(time.Now().UnixNano())
// Generate a random int32
randomInt := rand.Int31()
// Create a new user notification
newUserNotification := &pb.NotificationMessage{
Title: "A new family member",
Description: user.Name + " just arrived, send 'hi' message to connect.",
Id: int32(randomInt),
}
// Send the notification to the channel
notificationCh <- newUserNotification
}
if err := changeStream.Err(); err != nil {
log.Println("Error in change stream:", err)
}
}
-
Now just have to define rpc GetNotification
in gapi
. so create a new file notification.go
in package gapi
.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package gapi
import (
"fmt"
"github.com/djsmk123/server/pb"
"github.com/djsmk123/server/services"
)
// GetNotifications is a gRPC service method that streams notifications to the client.
func (server *Server) GetNotifications(req *pb.EmptyRequest, stream pb.GrpcServerService_GetNotificationsServer) error {
fmt.Println("Notification service started")
// Create a channel for receiving notifications
notificationCh := make(chan *pb.NotificationMessage)
// Start the background service to listen for new user additions
go services.NotificationNewUser(server.dbCollection.Users, notificationCh)
fmt.Println("Notification service created")
// Continuously listen for events
for {
select {
case <-stream.Context().Done():
// Client disconnected, close the channel and exit
close(notificationCh)
return nil
case notification, ok := <-notificationCh:
if !ok {
// The channel has been closed, exit the loop
return nil
}
// Send the received notification to the client
if err := stream.Send(&pb.NotificationMessage{
Title: notification.Title,
Id: int32(notification.Id),
Description: notification.Description,
}); err != nil {
return err
}
}
}
}
Wait, we can't listen change stream in mongodb
without converting standalone to replica set more info here.
In terminal open path (windows)
cd C:\Program Files\MongoDB\Server\<mongo-version>\bin
- Execute this command
mongod --port 27017 --dbpath="C:\Program Files\MongoDB\Server<mongo-version>\data" --replSet rs0 --bind_ip localhost
- Re-run mongo service
`rs.initiate()`
- Run server and let's test in `evans-cli`.
## Fluttering into Action: Your Magical Streaming App
- Add following dependencies to flutter project
dependencies:
flutter_background_service: ^5.0.1
flutter_local_notifications: ^15.1.1
flutter_background_service_android: ^6.0.1
- Create equivalent code for dart
protoc --proto_path=proto --dart_out=grpc:lib/pb proto/*.proto
- Create new services,called `notification.dart`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ignore_for_file: depend_on_referenced_packages
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_app/pb/empty_request.pb.dart';
import 'package:flutter_app/pb/rpc_notifications.pb.dart';
import 'package:flutter_app/services/grpc_services.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:shared_preferences/shared_preferences.dart';
class NotificationServices {
static String notificationChannelId = 'user-notification-channel';
static int notificationId = 888;
static String notificationService = "notification-service";
// Initialize the background notification service.
static Future<void> initializeService() async {
final service = FlutterBackgroundService();
AndroidNotificationChannel channel = AndroidNotificationChannel(
notificationChannelId, // id
notificationService,
description:
'This channel is used for important notifications.', // description
importance: Importance.high,
);
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
if (Platform.isIOS || Platform.isAndroid) {
await flutterLocalNotificationsPlugin.initialize(
const InitializationSettings(
iOS: DarwinInitializationSettings(),
android: AndroidInitializationSettings('ic_bg_service_small'),
),
);
}
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestPermission();
await service.configure(
androidConfiguration: AndroidConfiguration(
onStart: onStart, // Function to execute when service starts.
autoStart: true, // Automatically start the service.
isForegroundMode: false,
notificationChannelId:
notificationChannelId, // Notification channel ID to use.
autoStartOnBoot: true, // Automatically start on device boot.
),
iosConfiguration: IosConfiguration(
autoStart: true,
onForeground: onStart,
onBackground: onIosBackground,
),
);
service.startService();
}
// Function to execute when the service is running in the background on iOS.
static Future<bool> onIosBackground(ServiceInstance service) async {
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
SharedPreferences preferences = await SharedPreferences.getInstance();
await preferences.reload();
final log = preferences.getStringList('log') ?? <String>[];
log.add(DateTime.now().toIso8601String());
await preferences.setStringList('log', log);
return true;
}
// Function to execute when the service starts (in the background or foreground).
static Future<void> onStart(ServiceInstance service) async {
DartPluginRegistrant.ensureInitialized();
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
if (service is AndroidServiceInstance) {
service.on('setAsForeground').listen((event) {
service.setAsForegroundService();
});
service.on('setAsBackground').listen((event) {
service.setAsBackgroundService();
});
}
service.on("stop_service").listen((event) async {
await service.stopSelf();
});
if (service is AndroidServiceInstance) {
final res = getNotification();
res.listen((event) {
flutterLocalNotificationsPlugin.show(
notificationId,
event.title,
event.description,
NotificationDetails(
android: AndroidNotificationDetails(
notificationChannelId, notificationService,
icon: 'ic_bg_service_small', ongoing: false, autoCancel: true),
),
);
});
}
}
// Stream that receives NotificationMessage from the gRPC service.
static Stream<NotificationMessage> getNotification() async* {
final request = EmptyRequest();
final responseStream = GrpcService.client.getNotifications(request);
await for (var notification in responseStream) {
// Yield each received NotificationMessage.
yield notification;
}
}
}
- Call notification service function in
splash_screen.dart
Future initAsync() async {
try {
await NotificationServices.initializeService();
//keep same
} catch (e) {
log(e.toString());
navigateToLogin();
}
}
- Let's build application and listen for new user.

Thank you for joining us on this journey so far! We've covered a lot in this series, from setting up the server-side of our application in Golang, handling notifications, to building a foundation for communication.
In the next and final part of this series, we'll explore one of the most exciting features of gRPC: bi-directional streaming. We'll be implementing a real-time chat service using Flutter and Golang. Get ready for a dynamic and interactive experience that showcases the power and flexibility of gRPC.
So, stay tuned and keep coding! We can't wait to see you in the last part of this series where we'll bring it all together and create a fully functional chat application.
If you have any questions or feedback, please feel free to reach out. Happy coding, and see you in the next installment!
## Source code :
[Github Repo](https://github.com/Djsmk123/wtf-is-grpc/tree/part2)
## Follow me on
- [Twitter](https://twitter.com/smk_winner)
- [Instagram](https://www.instagram.com/smkwinner/)
- [Github](https://www.github.com/djsmk123)
- [linkedin](https://www.linkedin.com/in/md-mobin-bb928820b/)
- [dev.to](https://dev.to/djsmk123)
- [Medium](https://medium.com/@djsmk123)
Top comments (2)
Hello! I'm new to the DEV community, but I saw your post about Golang. I'm wondering which one is better for development and getting a job in 2023 ! Java Or Golang . In my country, Java has a 4x bigger community than Go, so I'm not sure which one to choose.
Thanks For your Time :D
Always go there where you have more opportunity so go for java. Once you learn Java I don't think it will much hard to switch to other like golang or other.