Question #178EasyFlutter Basics

What is protobuf and how we can use it with flutter ? (sample plugins protoc_plugin , protobuf)

#flutter

Answer

Overview

Protocol Buffers (protobuf) is a binary serialization format from Google, more efficient than JSON. Flutter can use protobuf for fast, compact data exchange with APIs.


What is Protobuf?

Protocol Buffers serialize structured data into binary format, making it:

  • Faster (binary vs JSON text)
  • Smaller (less network bandwidth)
  • Type-safe (schema-defined)

Protobuf vs JSON

JSON:

json
{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com"
}

Protobuf (binary):

text
{Alicealice@example.com
FeatureJSONProtobuf
FormatTextBinary
SizeLargerSmaller (30-50% less)
SpeedSlowerFaster (parsing)
Human-readable✅ Yes❌ No
Type-safe❌ No✅ Yes

Setup in Flutter

1. Install Dependencies

yaml
dependencies:
  protobuf: ^3.1.0

dev_dependencies:
  protoc_plugin: ^21.1.0

2. Install Protobuf Compiler

bash
# macOS
brew install protobuf

# Linux
sudo apt install protobuf-compiler

# Windows
# Download from https://github.com/protocolbuffers/protobuf/releases

3. Activate Dart Protobuf Plugin

bash
flutter pub global activate protoc_plugin

4. Add to PATH

bash
export PATH="$PATH:$HOME/.pub-cache/bin"

Define Protobuf Schema

Create

text
.proto
file defining message structure.

proto/user.proto:

protobuf
syntax = "proto3";

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  repeated string hobbies = 4;
}

message UserList {
  repeated User users = 1;
}

Generate Dart Code

bash
# Generate Dart code from .proto file
protoc --dart_out=lib/generated -Iproto proto/user.proto

Generated files:

  • text
    lib/generated/user.pb.dart
    (Dart classes)
  • text
    lib/generated/user.pbenum.dart
    (enums)
  • text
    lib/generated/user.pbjson.dart
    (JSON conversion)

Usage in Flutter

Serialize (Dart → Binary)

dart
import 'package:protobuf/protobuf.dart';
import 'generated/user.pb.dart';

void main() {
  // Create protobuf message
  final user = User()
    ..id = 123
    ..name = 'Alice'
    ..email = 'alice@example.com'
    ..hobbies.addAll(['Reading', 'Coding']);

  // Serialize to binary
  final bytes = user.writeToBuffer();
  print('Binary size: ${bytes.length} bytes');

  // Serialize to JSON (if needed)
  final json = user.writeToJson();
  print('JSON: $json');
}

Deserialize (Binary → Dart)

dart
// Deserialize from binary
final user = User.fromBuffer(bytes);
print('Name: ${user.name}');
print('Email: ${user.email}');

// Deserialize from JSON
final userFromJson = User()..mergeFromJson(json);
print('Hobbies: ${userFromJson.hobbies}');

Example: API Communication

Server Side (Node.js)

javascript
const protobuf = require('protobufjs');

// Load .proto file
const root = protobuf.loadSync('user.proto');
const User = root.lookupType('User');

// Create message
const message = User.create({
  id: 123,
  name: 'Alice',
  email: 'alice@example.com',
  hobbies: ['Reading', 'Coding']
});

// Serialize to binary
const buffer = User.encode(message).finish();

// Send binary data
res.send(buffer);

Flutter Client

dart
import 'package:http/http.dart' as http;
import 'generated/user.pb.dart';

Future<User> fetchUser() async {
  final response = await http.get(Uri.parse('https://api.example.com/user/123'));

  if (response.statusCode == 200) {
    // Deserialize binary response
    final user = User.fromBuffer(response.bodyBytes);
    return user;
  } else {
    throw Exception('Failed to load user');
  }
}

// Usage
final user = await fetchUser();
print('User: ${user.name}, ${user.email}');

Complete Example: User List

proto/user.proto:

protobuf
syntax = "proto3";

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

message UserListResponse {
  repeated User users = 1;
}

Generate:

bash
protoc --dart_out=lib/generated -Iproto proto/user.proto

Flutter Code:

dart
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'generated/user.pb.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(home: UserListScreen());
  }
}

class UserListScreen extends StatefulWidget {
  
  _UserListScreenState createState() => _UserListScreenState();
}

class _UserListScreenState extends State<UserListScreen> {
  List<User> _users = [];

  
  void initState() {
    super.initState();
    _fetchUsers();
  }

  Future<void> _fetchUsers() async {
    final response = await http.get(Uri.parse('https://api.example.com/users'));

    if (response.statusCode == 200) {
      final userListResponse = UserListResponse.fromBuffer(response.bodyBytes);

      setState(() {
        _users = userListResponse.users;
      });
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Users (Protobuf)')),
      body: ListView.builder(
        itemCount: _users.length,
        itemBuilder: (context, index) {
          final user = _users[index];
          return ListTile(
            title: Text(user.name),
            subtitle: Text(user.email),
          );
        },
      ),
    );
  }
}

Advanced Features

Nested Messages

protobuf
message Address {
  string street = 1;
  string city = 2;
}

message User {
  int32 id = 1;
  string name = 2;
  Address address = 3;  // Nested message
}

Usage:

dart
final user = User()
  ..id = 123
  ..name = 'Alice'
  ..address = (Address()
    ..street = '123 Main St'
    ..city = 'New York');

Enums

protobuf
enum UserType {
  UNKNOWN = 0;
  ADMIN = 1;
  USER = 2;
}

message User {
  int32 id = 1;
  string name = 2;
  UserType type = 3;
}

Usage:

dart
final user = User()
  ..id = 123
  ..name = 'Alice'
  ..type = UserType.ADMIN;

if (user.type == UserType.ADMIN) {
  print('Admin user');
}

Optional Fields

protobuf
message User {
  int32 id = 1;
  string name = 2;
  optional string nickname = 3;  // Optional field
}

Usage:

dart
final user = User()
  ..id = 123
  ..name = 'Alice';
  // nickname not set

if (user.hasNickname()) {
  print('Nickname: ${user.nickname}');
} else {
  print('No nickname');
}

Build Automation

Makefile:

makefile
generate:
	protoc --dart_out=lib/generated -Iproto proto/*.proto

clean:
	rm -rf lib/generated

rebuild: clean generate

Run:

bash
make generate

gRPC with Protobuf

For RPC-style APIs, use gRPC (built on protobuf).

yaml
dependencies:
  grpc: ^3.2.0

proto/user_service.proto:

protobuf
syntax = "proto3";

service UserService {
  rpc GetUser(GetUserRequest) returns (UserResponse);
}

message GetUserRequest {
  int32 id = 1;
}

message UserResponse {
  User user = 1;
}

Performance Comparison

Test: Serialize/deserialize 1000 users

FormatSizeSerialize TimeDeserialize Time
JSON125 KB150ms180ms
Protobuf45 KB50ms60ms

Result: Protobuf is ~3x smaller and ~3x faster.


When to Use Protobuf

High-performance APIs (mobile, microservices) ✅ Large data payloads (reduce bandwidth) ✅ Type-safe communication (schema enforcement) ✅ gRPC services (protobuf-native) ❌ Simple REST APIs (JSON is easier) ❌ Human-readable debugging (use JSON)


Best Practices

protobuf
// ✅ Use clear naming
message UserProfile { ... }

// ✅ Use optional for nullable fields
optional string nickname = 3;

// ✅ Use repeated for lists
repeated string hobbies = 4;

// ❌ Don't change field numbers (breaks compatibility)
// int32 id = 1;  // Keep field numbers stable

Summary

FeatureProtobufJSON
FormatBinaryText
SizeSmallLarge
SpeedFastSlower
Human-readableNoYes
Type-safeYesNo
Use CaseHigh-performance APIsSimple REST APIs

Key Takeaway: Use protobuf for high-performance, bandwidth-sensitive mobile apps.

Learn more: