Seamless communication with gRPC and Flutter: developer guide | by Scaibu | December 2024
This guide shows how to integrate gRPC with Flutter, using Firebase Firestore for data storage. Learn how to set up a scalable structure, maintain clean code, and create a responsive user interface for effective data management.
December 19, 2024
Imagine a world where your Flutter app communicates with a server as easily as calling a local function. It is the promise of gRPCa high-performance, open-source RPC framework that lets you define services, exchange data efficiently, and scale transparently. In this guide, we’ll learn how to integrate gRPC with Flutter, organize your project for scalability, and add a Firebase database to manage online data. With a focus on loose coupling, we will ensure that your application is modular and easy to maintain.
Whether you’re building a live chat app, a stock tracker, or an IoT dashboard, this guide has you covered.
- High performance: gRPC leverages HTTP/2 and Protobuf for fast and efficient communication.
- Cross-platform compatibility: Connect your Flutter app to various backends.
- Secure type contracts: Avoid runtime errors with strongly typed APIs.
- Scalability: Perfect for microservices and modern distributed systems.
Let’s see how you can leverage these benefits in your Flutter app.
Recommended file and folder structure
Organizing your project effectively is crucial for scalability and maintainability. Below is an optimized structure for a gRPC-based Flutter application:
lib/
|-- grpc/
| |-- protos/
| | |-- user.proto
| |-- generated/
| | |-- user.pb.dart
| | |-- user.pbgrpc.dart
| |-- grpc_client.dart
|
|-- models/
| |-- user_model.dart
|
|-- repositories/
| |-- user_repository.dart
|
|-- services/
| |-- user_service.dart
|
|-- screens/
| |-- user_screen.dart
|
|-- main.dart
pubspec.yaml
Step 1: Add Dependencies
Update pubspec.yaml
with the following dependencies:
dependencies:
flutter:
sdk: flutter
grpc: ^3.0.0
firebase_core: ^3.4.0
cloud_firestore: ^4.9.1dev_dependencies:
protobuf: ^2.0.0
build_runner: ^2.4.6
grpc_tools: ^2.0.0
Run:
flutter pub get
Step 2: Define Protobuf File
Create lib/grpc/protos/user.proto
:
syntax = "proto3";service UserService {
rpc GetUser (UserRequest) returns (UserResponse);
rpc SaveUser (User) returns (SaveUserResponse);
}
message UserRequest {
string user_id = 1;
}
message UserResponse {
string user_id = 1;
string name = 2;
string email = 3;
}
message User {
string user_id = 1;
string name = 2;
string email = 3;
}
message SaveUserResponse {
bool success = 1;
}
Step 3: Generate Dart Code
Run the following command to generate gRPC code:
flutter pub run build_runner build
This will generate:
user.pb.dart
user.pbgrpc.dart
Place them in lib/grpc/generated/
.
Step 4: Create a gRPC client
In lib/grpc/grpc_client.dart
:
import 'package:grpc/grpc.dart';
import 'generated/user.pbgrpc.dart';class GrpcClient {
late final UserServiceClient userServiceClient;
GrpcClient() {
final channel = ClientChannel(
'localhost',
port: 50051,
options: const ChannelOptions(credentials: ChannelCredentials.insecure()),
);
userServiceClient = UserServiceClient(channel);
}
Future<UserResponse> getUser(String userId) async {
final request = UserRequest()..userId = userId;
return await userServiceClient.getUser(request);
}
Future<SaveUserResponse> saveUser(String userId, String name, String email) async {
final user = User()
..userId = userId
..name = name
..email = email;
return await userServiceClient.saveUser(user);
}
}
Step 5: Firebase Integration for Online Data Storage
Step 5.1: Initialize Firebase
Update main.dart
:
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'screens/user_screen.dart';void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'gRPC with Flutter',
theme: ThemeData(primarySwatch: Colors.blue),
home: UserScreen(),
);
}
}
Step 5.2: Save and Recover Data Using Firestore
In lib/repositories/user_repository.dart
:
import 'package:cloud_firestore/cloud_firestore.dart';class UserRepository {
final _firestore = FirebaseFirestore.instance;
Future<void> saveUserToFirestore(String userId, String name, String email) async {
await _firestore.collection('users').doc(userId).set({
'name': name,
'email': email,
});
}
Future<Map<String, dynamic>?> fetchUserFromFirestore(String userId) async {
final doc = await _firestore.collection('users').doc(userId).get();
return doc.data();
}
}
Step 6: Loose coupling with services
In lib/services/user_service.dart
:
import '../grpc/grpc_client.dart';
import '../repositories/user_repository.dart';
import '../models/user_model.dart';class UserService {
final GrpcClient grpcClient;
final UserRepository userRepository;
UserService(this.grpcClient, this.userRepository);
Future<UserModel> getUser(String userId) async {
final response = await grpcClient.getUser(userId);
return UserModel.fromResponse(response);
}
Future<void> saveUser(UserModel user) async {
await grpcClient.saveUser(user.userId, user.name, user.email);
await userRepository.saveUserToFirestore(user.userId, user.name, user.email);
}
}
In lib/screens/user_screen.dart
:
import 'package:flutter/material.dart';
import '../services/user_service.dart';
import '../grpc/grpc_client.dart';
import '../repositories/user_repository.dart';
import '../models/user_model.dart';class UserScreen extends StatefulWidget {
@override
_UserScreenState createState() => _UserScreenState();
}
class _UserScreenState extends State<UserScreen> {
late final UserService userService;
UserModel? user;
bool isLoading = false;
@override
void initState() {
super.initState();
final grpcClient = GrpcClient();
final userRepository = UserRepository();
userService = UserService(grpcClient, userRepository);
}
Future<void> fetchUser() async {
setState(() => isLoading = true);
try {
user = await userService.getUser("123");
} catch (e) {
print("Error fetching user: $e");
} finally {
setState(() => isLoading = false);
}
}
Future<void> saveUser() async {
if (user == null) return;
try {
await userService.saveUser(user!);
} catch (e) {
print("Error saving user: $e");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("User Details"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isLoading)
const Center(child: CircularProgressIndicator())
else if (user != null) ...[
Text("ID: ${user!.userId}", style: const TextStyle(fontSize: 18)),
Text("Name: ${user!.name}", style: const TextStyle(fontSize: 18)),
Text("Email: ${user!.email}", style: const TextStyle(fontSize: 18)),
] else
const Text("No user data available.", style: TextStyle(fontSize: 18)),
const SizedBox(height: 20),
ElevatedButton(
onPressed: fetchUser,
child: const Text("Fetch User"),
),
ElevatedButton(
onPressed: saveUser,
child: const Text("Save User"),
),
],
),
),
);
}
}
Imagine an application that manages a dynamic list of users for a business, syncing between devices and enabling local and remote updates. Using gRPC for communication with the server and Fire base for online persistence guarantees:
- Real-time synchronization: Firebase’s real-time capabilities help sync user data across devices.
- Scalability: gRPC ensures efficient communication, even as the user base grows.
- Modularity: The application architecture makes it easy to add new features, such as user roles or detailed analytics.
- gRPC and floating: Combine for high-performance, secure cross-platform communication.
- Firebase integration: Add cloud-based persistence for a seamless online experience.
- Loose coupling: Organize services and repositories for better maintainability.
This setup is a powerhouse for building scalable, efficient, and modern Flutter apps. Happy coding! 🚀
Let me know if you need any further refinements or elaborations!
Thank you for reading to the end. Before leaving: