DEV Community

Cover image for WTF is Grpc? Part 3: Real time Chat ft. Flutter and Golang
Md. Mobin
Md. Mobin

Posted on

8 1

WTF is Grpc? Part 3: Real time Chat ft. Flutter and Golang

Welcome back, dear readers, to the world of "Wtf is gRPC?" – where we turn tech into a comedy show! 🚀

In Part 3, we're diving into real-time chat with the grace of caffeine-fueled squirrels and the humor of rubber chickens at a stand-up gig!

Get ready to leave slow chats in the dust and make your app as snappy as a squirrel on roller skates. Grab your code editor, sense of humor, and let's chat it up in style. Part 3 is gonna be chat-tastic! 🍰💬😄

grpc-socket-meme

Table of Contents: Part 3 - Real-Time Chat Extravaganza 🎉

Introduction to gRPC Bi-directional: The Magic of Two-Way Chats

As we've journeyed through gRPC, we first delved into unary communication, which is essentially one-way communication from the client to the server. Then, we ventured into the world of server-side streaming.

Now, let's dive into gRPC Bi-directional communication. This is where things get really interesting! In Bi-directional streaming RPC, both the client and server engage in a lively conversation by sending a continuous stream of messages back and forth.

Here's how it works:

  • The client initiates the action by setting up an HTTP stream with some initial header frames.
  • Once this connection is established, both the client and the server can send messages simultaneously, without waiting for the other party to finish. It's like a dynamic chat where no one has to take turns.

Think of gRPC as the superhero chat line. It's like Tony's suit and Bruce's lab assistant, J.A.R.V.I.S., having a direct line to each other.

Socket vs. gRPC Bi-directional: A Cinematic Showdown" 🍿🎥

Aspect Socket gRPC Bi-directional Top-Rated Movie Comparison
Setup Complexity Generally low, traditional sockets require less setup Requires setting up gRPC services and protocols, which can be more complex Like the simplicity of "The Shawshank Redemption"
Communication Type Low-level and can be used for any data format High-level, designed for structured data transmission Similar to the plot intricacies of "The Godfather"
Performance Can be efficient for simple data exchange Optimized for high-performance, especially in microservices Like the action-packed "Mad Max: Fury Road"
Bi-directional Possible with added complexity and custom handling Inherently supports efficient bi-directional communication Much like "Pulp Fiction's" non-linear storytelling
Error Handling Requires custom error handling Provides built-in status codes and error handling Comparable to the suspense in "The Silence of the Lambs"
Cross-Language Supports multiple programming languages Multi-language support with protocol buffers Like the multilingual charm of "Inglourious Basterds"
Integration Ease Requires more manual integration Easier integration into gRPC-compatible systems Much like the seamless blend in "The Dark Knight"

grpc bi-directional

Setup Your Server: Where the Real-Time Chat Spells Begin

  • Let's continue code from part 2,where we left.


git clone git@github.com:Djsmk123/Wtf-is-grpc.git


Enter fullscreen mode Exit fullscreen mode
  • Switch to part-2 branch(for starter code) ```

git switch part2

- As we are going to create Chat one to one service for that we required to get all the users and then communicate.

- Create profobuf for getting users(`rpc_users.proto`)

syntax="proto3";
package pb;
import "user.proto";
option go_package="github.com/djsmk123/server/pb";
message UsersListRequest{
int32 page_number = 1;
int32 page_size = 2;
optional string name =3;
}
message ListUserMessage{
int32 total_count =1;
repeated User users=2;
}
view raw rpc_users.proto hosted with ❤ by GitHub
  • Add GetUsers rpc service in GRPCServerService
Enter fullscreen mode Exit fullscreen mode

// add this line to import
import "rpc_users.proto";

service GrpcServerService {
// add this line too
rpc GetUsers(UsersListRequest) returns (ListUserMessage) {};
}


- Generate equivalent code for server(`Golang`)

Enter fullscreen mode Exit fullscreen mode

protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative \
--go-grpc_out=pb --go-grpc_opt=paths=source_relative \
proto/*.proto


- Add function in `auth` package `get_users.go`


package auth
import (
"context"
"github.com/djsmk123/server/db/model"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func GetUsers(collection *mongo.Collection, context context.Context, page int, pageSize int, query *string, selfUserName string) ([]*model.UserModel, error) {
// Define filters based on the query parameter (if provided) and exclude the self user
filter := bson.M{}
if query != nil {
filter["name"] = bson.M{"$regex": query, "$options": "i"} // Case-insensitive regex search on 'name' field
}
filter["username"] = bson.M{"$ne": selfUserName} // Exclude the self user
// Set options for pagination
findOptions := options.Find()
findOptions.SetLimit(int64(pageSize))
findOptions.SetSkip(int64((page - 1) * pageSize))
// Perform the query
cursor, err := collection.Find(context, filter, findOptions)
if err != nil {
return nil, err
}
defer cursor.Close(context)
var users []*model.UserModel
for cursor.Next(context) {
var user model.UserModel
if err := cursor.Decode(&user); err != nil {
return nil, err
}
users = append(users, &user)
}
if err := cursor.Err(); err != nil {
return nil, err
}
return users, nil
}
view raw get_users.go hosted with ❤ by GitHub
  • Let's call this function in grpc-server interface(package gapi) and add this function in auth.go.
func (server *Server) GetUsers(ctx context.Context, req *pb.UsersListRequest) (*pb.ListUserMessage, error) {
payload, ok := ctx.Value(payloadHeader).(*token.Payload)
if !ok {
return nil, status.Errorf(codes.Internal, "missing required token")
}
users, err := auth.GetUsers(server.dbCollection.Users, context.TODO(), int(req.GetPageNumber()), int(req.GetPageSize()), req.Name, payload.Username)
if err != nil {
return nil, status.Errorf(codes.Internal, err.Error())
}
pbUsers := []*pb.User{}
for _, user := range users {
pbUser := auth.ConvertUserObjectToUser(user)
pbUsers = append(pbUsers, pbUser)
}
return &pb.ListUserMessage{
TotalCount: int32(len(pbUsers)),
Users: pbUsers,
}, nil
}
view raw auth.go hosted with ❤ by GitHub
  • We are done with this. But we required users so that we can send message so don't, I am providing list of users (around 35), just load them into mongodb.

users-data.json

> Note: Password for each user is 12345678.

Setting Up Your Chat Service:

Establishing a chat service is a straightforward procedure.

  • RPC Service Creation : Creating an RPC (Remote Procedure Call) service is at the core of this setup.
  • Bidirectional Message Stream: Implement a bidirectional stream of messages.Messages are initiated by the client and sent to the server.
  • Bidirectional Response Stream: Create a bidirectional stream for responses.Responses are generated by the server and sent back to the client.
  • Acknowledgment Mechanism: Before actual communication begins, an acknowledgment mechanism is necessary to connect both the client and the server.This mechanism helps ensure a successful connection.
  • Database Handling: Notably, the acknowledgment message is designed to be transient and is not stored in the database.
  • Confirmation of Connection: The acknowledgment message serves as a confirmation of a successful connection.

  • Setting up protos: Setup proto for Sending message and get messages

syntax="proto3";
package pb;
option go_package="github.com/djsmk123/server/pb";
import "google/protobuf/timestamp.proto";
message Message{
string id=1;
string sender=2;
string receiver=3;
string message=4;
google.protobuf.Timestamp created_at = 5;
}
message SendMessageRequest{
string message=1;
string reciever=2;
}
message GetAllMessagesRequest{
string reciever=1;
}
message GetAllMessagesResponse{
repeated Message messages=1;
}
view raw rpc_chat.proto hosted with ❤ by GitHub
  • Add these rpc function into service
Enter fullscreen mode Exit fullscreen mode

service GrpcServerService {
//add these lines
rpc SendMessage(stream SendMessageRequest) returns (stream Message){};
rpc GetAllMessage(GetAllMessagesRequest) returns (GetAllMessagesResponse){};
}

- Generate equivalent code

Enter fullscreen mode Exit fullscreen mode

protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative \
--go-grpc_out=pb --go-grpc_opt=paths=source_relative \
proto/*.proto


- We need JWT-based authentication for both receiving and sending messages. While there are two types of interceptors available, namely `unary interceptors` and `stream interceptors`, we currently utilize the `unary interceptor` as our middleware and lets add middleware for stream service too.

![user-auth](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uze08vugtzoi7h5mkqw2.jpg)

- update `middleware.go` in `gapi` package.

package gapi
import (
"context"
"strings"
"github.com/djsmk123/server/token"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
const (
authorizationHeader = "authorization"
authorizationBearer = "bearer"
payloadHeader = "payload"
)
func (server *Server) UnaryAuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
ctx, err := server.AuthInterceptor(info.FullMethod, ctx)
if err != nil {
return nil, err
}
return handler(ctx, req)
}
func (server *Server) AuthInterceptor(method string, ctx context.Context) (context.Context, error) {
if methodRequiresAuthentication(method) {
// Extract the metadata from the context.
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "metadata not found")
}
// Get the authorization token from metadata if present.
authTokens := md[authorizationHeader]
if len(authTokens) == 0 {
// No token found, but it's optional, so return the unmodified context.
return ctx, nil
}
authHeader := authTokens[0] // Assuming a single token is sent in the header.
fields := strings.Fields(authHeader)
if len(fields) < 2 {
return nil, status.Errorf(codes.Unauthenticated, "invalid auth header format: %v", fields)
}
authType := strings.ToLower(fields[0])
if authType != authorizationBearer {
return nil, status.Errorf(codes.Unauthenticated, "invalid authorization type: %v", authType)
}
accessToken := fields[1]
payload, err := server.tokenMaker.VerifyToken(accessToken)
if err != nil {
if err == token.ErrInvalidToken {
return nil, status.Errorf(codes.Unauthenticated, "invalid token %v", authType)
}
if err == token.ErrExpiredToken {
return nil, status.Errorf(codes.Unauthenticated, "token %v expired", authType)
}
}
ctx = context.WithValue(ctx, payloadHeader, payload)
}
return ctx, nil
}
type customServerStream struct {
grpc.ServerStream
ctx context.Context
}
func (css *customServerStream) Context() context.Context {
return css.ctx
}
func (server *Server) StreamAuthInterceptor(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := stream.Context()
ctx, err := server.AuthInterceptor(info.FullMethod, ctx)
if err != nil {
return err
}
newStream := &customServerStream{
ServerStream: stream,
ctx: ctx,
}
return handler(srv, newStream)
}
view raw middlware.go hosted with ❤ by GitHub
  • Register middleware in main.go
Enter fullscreen mode Exit fullscreen mode

//update this line
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(server.UnaryAuthInterceptor),
grpc.StreamInterceptor(server.StreamAuthInterceptor),
)


- Now create `Message` Object for `mongodb` collection

package model
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Message struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
Sender string `bson:"sender"`
Receiver string `bson:"receiver"`
Message string `bson:"message"`
CreatedAt time.Time `bson:"created_at"`
}
view raw chat.go hosted with ❤ by GitHub
  • Update MongoCollection
Enter fullscreen mode Exit fullscreen mode

type MongoCollections struct {
Users *mongo.Collection
Chats *mongo.Collection
}


- Before going to into complex part to listen message and store them into db, lets deal with simple rpc function `GetAllMessage` from db and `SendMessage` to store the message.

package services
import (
"context"
"strings"
"time"
"github.com/djsmk123/server/auth"
"github.com/djsmk123/server/db"
"github.com/djsmk123/server/db/model"
"github.com/djsmk123/server/pb"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)
func SendMessage(ctx context.Context, messsage string, sender string, reciever string, database *db.MongoCollections) (*model.Message, error) {
//check if reciever is exist or not
//check if receiver and sender are the same or not
if strings.EqualFold(sender, reciever) {
return nil, status.Errorf(codes.InvalidArgument, "sender and receiver must not be the same")
}
_, err := auth.GetUser(database.Users, ctx, reciever)
if err != nil {
if err == auth.ErrUserNotFound {
return nil, status.Errorf(codes.NotFound, "user not found")
}
return nil, status.Errorf(codes.Internal, "something went wrong")
}
newMessage := &model.Message{
Sender: sender,
Receiver: reciever,
Message: messsage,
CreatedAt: time.Now(),
}
result, err := database.Chats.InsertOne(ctx, newMessage)
insertedID, ok := result.InsertedID.(primitive.ObjectID)
if !ok {
return nil, status.Errorf(codes.Internal, "failed to get inserted ID")
}
newMessage.ID = insertedID
if err != nil {
return nil, err
}
return newMessage, nil
}
func GetAllMessage(ctx context.Context, db *db.MongoCollections, sender string, receiver string) (*pb.GetAllMessagesResponse, error) {
var messages []*pb.Message
//check if receiver and sender are the same or not
if strings.EqualFold(sender, receiver) {
return nil, status.Errorf(codes.InvalidArgument, "sender and receiver must not be the same")
}
_, err := auth.GetUser(db.Users, ctx, receiver)
if err != nil {
if err == auth.ErrUserNotFound {
return nil, status.Errorf(codes.NotFound, "user not found")
}
return nil, status.Errorf(codes.Internal, "something went wrong")
}
filter := bson.M{
"$or": []bson.M{
{"sender": sender, "receiver": receiver},
{"sender": receiver, "receiver": sender},
},
}
cursor, err := db.Chats.Find(ctx, filter)
if err != nil {
return nil, status.Errorf(codes.Internal, "something went wrong while fetching %v", err)
}
defer cursor.Close(ctx)
for cursor.Next(ctx) {
var message model.Message
if err := cursor.Decode(&message); err != nil {
return nil, err
}
messages = append(messages, &pb.Message{
Id: message.ID.Hex(),
Receiver: message.Receiver,
Sender: message.Sender,
Message: message.Message,
CreatedAt: timestamppb.New(message.CreatedAt),
})
}
return &pb.GetAllMessagesResponse{
Messages: messages,
}, nil
}
view raw chat_service.go hosted with ❤ by GitHub
  • Now we are going to use go-routine to listen and send message to the client

> 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

  • Let's Define SendMessage and GetAllMesasge for grpc server interface.
package gapi
import (
"context"
"fmt"
"io"
"log"
"time"
pb "github.com/djsmk123/server/pb"
"github.com/djsmk123/server/services"
"github.com/djsmk123/server/token"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)
// SendMessage handles sending messages.
func (server *Server) SendMessage(stream pb.GrpcServerService_SendMessageServer) error {
// Extract the user payload from the context.
payload, ok := stream.Context().Value(payloadHeader).(*token.Payload)
if !ok {
return status.Errorf(codes.Internal, "missing required token")
}
// Initialize the clients map if it's nil.
server.mu.Lock()
if server.clients == nil {
server.clients = make(map[string]pb.GrpcServerService_SendMessageServer)
}
server.clients[payload.Username] = stream
server.mu.Unlock()
// Continuously receive and forward messages.
for {
message, err := stream.Recv()
if err == io.EOF {
// The client has closed the connection.
break
}
if err != nil {
return status.Errorf(codes.Internal, "Error receiving message: %v", err)
}
if message.Message == "Join_room" {
// Special handling for "Join_room" message.
// Send a confirmation message back to the sender.
response := &pb.Message{
Sender: "Server", // You can set the sender to "Server" or any other identifier.
Receiver: payload.Username,
Message: "You have joined the room.",
CreatedAt: timestamppb.New(time.Now()),
}
if err := stream.Send(response); err != nil {
log.Printf("Error sending confirmation message: %v", err)
}
receiverConfirmation := &pb.Message{
Sender: "Server", // Sender is the server in this case.
Receiver: message.Reciever,
Message: fmt.Sprintf("%s has joined the room.", payload.Username),
CreatedAt: timestamppb.New(time.Now()),
}
server.mu.Lock()
receiver, ok := server.clients[message.Reciever]
server.mu.Unlock()
if ok {
// Send the notification to the receiver.
if err := receiver.Send(receiverConfirmation); err != nil {
log.Printf("Error sending notification to %s: %v", message.Reciever, err)
}
}
} else {
// Normal message handling.
res, err := services.SendMessage(stream.Context(), message.Message, payload.Username, message.Reciever, &server.dbCollection)
if err != nil {
return status.Errorf(codes.Internal, "Error saving message: %v", err)
}
// Find the receiver by username.
server.mu.Lock()
receiver, ok := server.clients[message.Reciever]
if !ok {
// If the receiver or sender is not found, send an error message back to the sender.
continue
}
sender, ok := server.clients[payload.Username]
server.mu.Unlock()
if !ok {
// If the receiver or sender is not found, send an error message back to the sender.
continue
}
// Forward the message to the receiver.
err = receiver.Send(&pb.Message{
Sender: payload.Username,
Receiver: message.Reciever,
Message: message.Message,
CreatedAt: timestamppb.New(time.Now()),
Id: res.ID.Hex(),
})
if err != nil {
log.Printf("Error sending message to %s: %v", message.Reciever, err)
continue
}
// Send the same message back to the sender as a confirmation.
err = sender.Send(&pb.Message{
Sender: payload.Username,
Receiver: message.Reciever,
Message: message.Message,
Id: res.ID.Hex(),
CreatedAt: timestamppb.New(time.Now()),
})
if err != nil {
log.Printf("Error sending confirmation message to %s: %v", payload.Username, err)
continue
}
}
}
// Remove the sender from the clients map when the client disconnects.
server.mu.Lock()
delete(server.clients, payload.Username)
server.mu.Unlock()
return nil
}
// GetAllMessage retrieves all messages for a user.
func (server *Server) GetAllMessage(ctx context.Context, req *pb.GetAllMessagesRequest) (*pb.GetAllMessagesResponse, error) {
// Extract the user payload from the context.
payload, ok := ctx.Value(payloadHeader).(*token.Payload)
if !ok {
return nil, status.Errorf(codes.Internal, "missing required token")
}
// Call the GetAllMessage service.
return services.GetAllMessage(ctx, &server.dbCollection, payload.Username, req.GetReciever())
}
view raw chat.go hosted with ❤ by GitHub

> Note: To make connection b/w two users, its required to send first message Join_room so that other user can get acknowledgment.

Testing gRPC in CLI

grpc-testing

Start with Flutter Application: Embark on Your Chatting Adventure

  • Add following dependency into your project:
Enter fullscreen mode Exit fullscreen mode

flutter pub add intl



- Generate equivalent code for dart from `protos`.

Enter fullscreen mode Exit fullscreen mode

protoc --proto_path=proto --dart_out=grpc:lib/pb proto/*.proto


- As it is known issue dart does not generate code `google/protobuf/timestamp.pb.dart`,[read here](https://github.com/google/protobuf.dart/issues/483). So download this file [google_timestamp.pb.dart](https://gist.github.com/Djsmk123/83025e60cbf2c5a931ffefdf2b3a5b7e) and update the generated code as required(just change import path)

- Before starting chat service we need to display all the users into home screen so that we can send message to other users

> PS: I am not going to talk about basic UI building for showing the list of users.

- Add function to get all users in `AuthService` class

Enter fullscreen mode Exit fullscreen mode

class AuthServices{
//keep same, add this function
static Future> getUsers(
{int pageNumber = 1, String? search}) async {
final res = await GrpcService.client.getUsers(
UsersListRequest(pageSize: 10, pageNumber: pageNumber, name: search),
options: CallOptions(metadata: {'authorization': 'bearer $authToken'}));
return res.users.map((e) => UserModel(e.id, e.username, e.name)).toList();
}
}

- Call this function in `HomeScreen()`,for reference use [this ui](https://gist.github.com/Djsmk123/3c39818c3a00c0b01ad8eadb36486102) or create your own.

- Let's Create chat service to get messages(History)

Enter fullscreen mode Exit fullscreen mode

class ChatService {
static Future> getMessages(String username) async {
final res = await GrpcService.client.getAllMessage(
GetAllMessagesRequest(
reciever: username,
),
options: CallOptions(
metadata: {'authorization': 'bearer ${AuthService.authToken}'}));
return res.messages;
}
}


- Let's listen for messages,send message and fetch history.

// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_app/models/user.dart';
import 'package:flutter_app/pb/rpc_chat.pb.dart';
import 'package:flutter_app/screens/widgets/reciver_message_widget.dart';
import 'package:flutter_app/screens/widgets/sender_message_widget.dart';
import 'package:flutter_app/services/auth.dart';
import 'package:flutter_app/services/chat_services.dart';
import 'package:flutter_app/services/grpc_services.dart';
import 'package:grpc/grpc.dart';
class MessageScreen extends StatefulWidget {
final UserModel reciever;
const MessageScreen({super.key, required this.reciever});
@override
State<MessageScreen> createState() => _MessageScreenState();
}
class _MessageScreenState extends State<MessageScreen> {
final TextEditingController controller = TextEditingController();
List<Message> messages = [];
bool isLoading = false;
final StreamController<SendMessageRequest> streamController =
StreamController<SendMessageRequest>();
final ScrollController scrollController = ScrollController();
String? error;
@override
void initState() {
super.initState();
initAsync();
}
initAsync() async {
await fetchChatsHistory();
startListeningMessageRequest();
addMessage("Join_room");
}
void startListeningMessageRequest() {
final stream = GrpcService.client.sendMessage(streamController.stream,
options: CallOptions(
metadata: {'authorization': 'bearer ${AuthService.authToken}'}));
stream.listen((value) {
if (value.sender != "Server") {
messages.add(value);
} else {
if (value.message.contains("has joined the room.")) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${widget.reciever.fullname} has joined now.'),
),
);
}
}
setState(() {});
});
}
void addMessage(String message) {
// Simulate adding a message to the stream when a button is clicked
final req = SendMessageRequest(
message: message,
reciever: widget.reciever.username,
);
streamController.sink.add(req);
}
void _sendMessage() {
final messageText = controller.text;
if (messageText.isNotEmpty) {
addMessage(messageText);
controller.clear();
scrollDown();
setState(() {});
}
}
fetchChatsHistory() async {
try {
isLoading = true;
setState(() {});
final res = await ChatService.getMessages(widget.reciever.username);
messages.addAll(res);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to get messages: $error'),
),
);
} finally {
setState(() {
isLoading = false;
});
}
}
@override
void dispose() {
streamController.close();
controller.dispose();
scrollController.dispose();
super.dispose();
}
void scrollDown() {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(seconds: 2),
curve: Curves.fastOutSlowIn,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
title: Text(
"${widget.reciever.fullname.replaceRange(0, 1, widget.reciever.fullname[0].toUpperCase())}'s"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
isLoading
? loadingWidget()
: (error != null
? errorWidget()
: messages.isNotEmpty
? Expanded(
child: ListView.builder(
shrinkWrap: true,
controller: scrollController,
itemCount: messages.length,
itemBuilder: ((context, index) {
Message message = messages[index];
bool isOwn = message.sender ==
AuthService.user?.username;
return isOwn
? SentMessageScreen(
message: message,
)
: ReceivedMessageScreen(message: message);
})),
)
: const Expanded(
child: Center(
child: Text(
"No message found,start conversion with 'hi' "),
),
)),
Container(
height: 80,
width: MediaQuery.of(context).size.width,
color: Colors.transparent,
child: Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.only(
left: 10, right: 10, bottom: 10),
margin: const EdgeInsets.only(left: 10, right: 10),
child: TextField(
maxLines: null,
controller: controller,
enabled: !isLoading,
decoration: InputDecoration(
prefixIcon: IconButton(
onPressed: () {},
icon: const Icon(Icons.message),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide:
const BorderSide(color: Colors.black)),
suffixIcon: IconButton(
onPressed: () {
_sendMessage();
},
icon: const Icon(Icons.send)),
hintText: 'Reply to this wave'),
onChanged: (value) {
if (value.isNotEmpty) {}
},
),
),
),
],
),
),
],
),
),
);
}
loadingWidget() => const Center(child: CircularProgressIndicator());
errorWidget() => Center(
child: Text(error ?? "Something went wrong",
style: const TextStyle(color: Colors.red)));
}

> Note: UI component maybe missing or you create your own. for reference check source code

  • Let's talk about what we did in above class.
    • In initState() we are starting to listen message by sending first message Join_room to make connection and another function fetchChatsHistory() as name suggested, getting earlier messages by calling grpc unary service.
    • on sending message we are adding message data to stream-controller to send stream of message to the server.

Outputs: The Hilarious Messages of Success

End of the series

Thank you to everyone who has supported me throughout this series. I promise to be more consistent next time. The blog series titled 'Wtf is gRPC?' comes to an end with this post. In Part 1, we delved into authentication and authorization using unary gRPC communication. In Part 2, we had some fun creating a custom notification service in Flutter, along with server-side streaming gRPC. And now, in the grand finale, Part 3, we explored bi-directional gRPC to create a simple real-time chat application. Thanks again, and remember, gRPC isn't as mysterious as it sounds – it's just a protocol! Stay tuned for more tech adventures!

Thank for 2k+ Followers on dev.to

Thanks You

flutter meme

Source code :

Github Repo

Follow me on

Enter fullscreen mode Exit fullscreen mode

Sentry mobile image

Improving mobile performance, from slow screens to app start time

Based on our experience working with thousands of mobile developer teams, we developed a mobile monitoring maturity curve.

Read more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay