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! 🍰💬😄
Table of Contents: Part 3 - Real-Time Chat Extravaganza 🎉
- Introduction to gRPC Bi-directional: The Magic of Two-Way Chats
- Setup Your Server: Where the Real-Time Chat Spells Begin
- Start with Flutter Application: Embark on Your Chatting Adventure
- Outputs: The Hilarious Messages of Success
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" |
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
- 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`)
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
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;
}
- Add
GetUsers
rpc service in GRPCServerService
// 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`)
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`
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 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
}
- Let's call this function in grpc-server interface(package
gapi
) and add this function in auth.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
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
}
- 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
.
> 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
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
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;
}
- Add these
rpc
function into service
service GrpcServerService {
//add these lines
rpc SendMessage(stream SendMessageRequest) returns (stream Message){};
rpc GetAllMessage(GetAllMessagesRequest) returns (GetAllMessagesResponse){};
}
- Generate equivalent code
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.
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 (
"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)
}
- Register middleware in
main.go
//update this line
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(server.UnaryAuthInterceptor),
grpc.StreamInterceptor(server.StreamAuthInterceptor),
)
- Now create `Message` Object for `mongodb` collection
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 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"`
}
- Update MongoCollection
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.
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"
"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
}
- 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.
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 (
"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())
}
> 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
Start with Flutter Application: Embark on Your Chatting Adventure
- Add following dependency into your project:
flutter pub add intl
- Generate equivalent code for dart from `protos`.
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
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)
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.
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: 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
Source code :
Follow me on
Top comments (0)