How to Use Protocol Buffers in Dart/Flutter

A practical guide to working with Protocol Buffers in Dart and Flutter mobile apps

Published: January 2025 • 9 min read

Protocol Buffers is perfect for Flutter apps. It provides smaller message sizes and faster serialization compared to JSON, which means better battery life, reduced data usage, and faster app performance. This is especially valuable for mobile apps where bandwidth and battery matter.

This guide shows you how to use Protocol Buffers in Dart and Flutter applications. We'll use simple examples and focus on real-world mobile app scenarios. Dart's clean syntax makes working with protobuf straightforward.

What You'll Need

  • Flutter/Dart: SDK 3.0+ (check with flutter --version)
  • Protocol Buffer Compiler: We'll install this next
  • protoc_plugin: Dart plugin for protoc

Step 1: Install Protocol Buffers

First, activate the Dart protoc plugin:

dart pub global activate protoc_plugin

Install the Protocol Buffer compiler. On macOS:

brew install protobuf

On Ubuntu/Debian:

sudo apt update
sudo apt install protobuf-compiler

# Verify installation
protoc --version

On Windows, download from GitHub releases and add to your PATH.

Step 2: Set Up Your Flutter Project

Create a new Flutter project:

flutter create flutter_protobuf_demo
cd flutter_protobuf_demo

mkdir protos
mkdir lib/generated

Add protobuf to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  protobuf: ^3.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter

Install dependencies:

flutter pub get

Step 3: Create Your .proto File

Create protos/subscriber.proto:

syntax = "proto3";

package telecom;

// Mobile subscriber information
message Subscriber {
  string msisdn = 1;           // Mobile number
  string name = 2;             // Subscriber name
  string email = 3;            // Email address
  SubscriptionType type = 4;   // Plan type
  bool active = 5;             // Account status
  repeated string services = 6; // Active services
}

enum SubscriptionType {
  PREPAID = 0;
  POSTPAID = 1;
  CORPORATE = 2;
}

Step 4: Generate Dart Code

Make sure the Dart protoc plugin is in your PATH:

export PATH="$PATH:$HOME/.pub-cache/bin"
# On Windows PowerShell: $env:PATH += ";$HOME\.pub-cache\bin"

Generate Dart code:

protoc --dart_out=lib/generated --proto_path=protos protos/subscriber.proto

This creates Dart files in lib/generated/.

Step 5: Use Protocol Buffers in Dart

Create a simple Dart script to test (lib/main.dart):

import 'generated/subscriber.pb.dart';

void main() {
  // Create a new subscriber
  final subscriber = Subscriber()
    ..msisdn = '+91-9876543210'
    ..name = 'Telecom Customer'
    ..email = 'customer@telecom.com'
    ..type = SubscriptionType.POSTPAID
    ..active = true
    ..services.addAll(['Voice', 'Data', '4G LTE']);

  print('Created subscriber: ${subscriber.name}');
  print('MSISDN: ${subscriber.msisdn}');
  print('Type: ${subscriber.type}');

  // Serialize to binary
  final bytes = subscriber.writeToBuffer();
  print('Serialized to ${bytes.length} bytes\n');

  // Deserialize from binary
  final decoded = Subscriber.fromBuffer(bytes);
  print('Deserialized subscriber: ${decoded.name}');
  print('Email: ${decoded.email}');
  print('Active: ${decoded.active}');

  // Access repeated fields
  print('\nActive services:');
  for (var service in decoded.services) {
    print('  - $service');
  }
}

Step 6: Run Your Application

Run the Dart script:

dart run lib/main.dart

You should see output like:

Created subscriber: Telecom Customer
MSISDN: +91-9876543210
Type: SubscriptionType.POSTPAID
Serialized to 68 bytes

Deserialized subscriber: Telecom Customer
Email: customer@telecom.com
Active: true

Active services:
  - Voice
  - Data
  - 4G LTE

Using Protobuf in a Flutter Widget

Here's how to use Protobuf in a Flutter UI:

import 'package:flutter/material.dart';
import 'generated/subscriber.pb.dart';

class SubscriberScreen extends StatefulWidget {
  @override
  _SubscriberScreenState createState() => _SubscriberScreenState();
}

class _SubscriberScreenState extends State<SubscriberScreen> {
  Subscriber? subscriber;

  @override
  void initState() {
    super.initState();
    _loadSubscriber();
  }

  void _loadSubscriber() {
    // Create subscriber (in real app, load from API)
    final sub = Subscriber()
      ..msisdn = '+91-9876543210'
      ..name = 'Telecom Customer'
      ..email = 'customer@telecom.com'
      ..type = SubscriptionType.POSTPAID
      ..active = true
      ..services.addAll(['Voice', 'Data', '4G LTE']);

    setState(() {
      subscriber = sub;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (subscriber == null) {
      return Scaffold(
        appBar: AppBar(title: Text('Subscriber')),
        body: Center(child: CircularProgressIndicator()),
      );
    }

    return Scaffold(
      appBar: AppBar(title: Text('Subscriber Details')),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Name: ${subscriber!.name}',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 8),
            Text('MSISDN: ${subscriber!.msisdn}'),
            Text('Email: ${subscriber!.email}'),
            Text('Type: ${subscriber!.type}'),
            Text('Status: ${subscriber!.active ? "Active" : "Inactive"}'),
            SizedBox(height: 16),
            Text(
              'Services:',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            ...subscriber!.services.map((service) => Text('  • $service')),
          ],
        ),
      ),
    );
  }
}

Sending Protobuf Over HTTP

Use Protobuf with HTTP requests in Flutter:

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

Future<void> sendSubscriber() async {
  // Create subscriber
  final subscriber = Subscriber()
    ..msisdn = '+91-9876543210'
    ..name = 'Mobile User'
    ..type = SubscriptionType.PREPAID;

  // Serialize to bytes
  final bytes = subscriber.writeToBuffer();

  // Send to server
  final response = await http.post(
    Uri.parse('https://api.example.com/subscribers'),
    headers: {
      'Content-Type': 'application/x-protobuf',
    },
    body: bytes,
  );

  if (response.statusCode == 200) {
    print('Subscriber sent successfully');
  }
}

Future<Subscriber?> fetchSubscriber(String msisdn) async {
  // Fetch from server
  final response = await http.get(
    Uri.parse('https://api.example.com/subscribers/$msisdn'),
    headers: {
      'Accept': 'application/x-protobuf',
    },
  );

  if (response.statusCode == 200) {
    // Parse protobuf response
    return Subscriber.fromBuffer(response.bodyBytes);
  }
  return null;
}

Convert Between JSON and Protobuf

Dart protobuf supports JSON conversion:

import 'dart:convert';
import 'generated/subscriber.pb.dart';

void jsonExample() {
  final subscriber = Subscriber()
    ..msisdn = '+91-9876543210'
    ..name = 'Mobile User'
    ..type = SubscriptionType.PREPAID
    ..active = true;

  // Convert to JSON
  final jsonString = jsonEncode(subscriber.toProto3Json());
  print('JSON output:');
  print(jsonString);

  // Parse from JSON
  final jsonData = {
    'msisdn': '+91-9111111111',
    'name': 'New Subscriber',
    'type': 'POSTPAID',
    'active': true,
  };

  final parsed = Subscriber()..mergeFromProto3Json(jsonData);
  print('\nParsed from JSON: ${parsed.name}');
  print('Type: ${parsed.type}');
}

Save to Local Storage

Save and load protobuf data from device storage:

import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'generated/subscriber.pb.dart';

Future<void> saveSubscriber(Subscriber subscriber) async {
  // Get app documents directory
  final directory = await getApplicationDocumentsDirectory();
  final file = File('${directory.path}/subscriber.bin');

  // Write protobuf bytes
  await file.writeAsBytes(subscriber.writeToBuffer());
  print('Saved to ${file.path}');
}

Future<Subscriber?> loadSubscriber() async {
  try {
    final directory = await getApplicationDocumentsDirectory();
    final file = File('${directory.path}/subscriber.bin');

    // Read and parse
    final bytes = await file.readAsBytes();
    return Subscriber.fromBuffer(bytes);
  } catch (e) {
    print('Error loading subscriber: $e');
    return null;
  }
}

// Usage in Flutter
void example() async {
  final subscriber = Subscriber()
    ..msisdn = '+91-9876543210'
    ..name = 'Saved User'
    ..type = SubscriptionType.POSTPAID;

  await saveSubscriber(subscriber);

  final loaded = await loadSubscriber();
  if (loaded != null) {
    print('Loaded: ${loaded.name}');
  }
}

Note: Don't forget to add path_provider to your pubspec.yaml dependencies.

Best Practices for Flutter

Use Protobuf for API Communication

Protobuf shines in mobile apps. Smaller message sizes mean less data usage, faster loading, and better battery life.

Cache Protobuf Locally

Save protobuf messages to device storage for offline access. They're more compact than JSON, saving storage space.

Use Cascade Notation

Dart's cascade operator (..) makes building protobuf messages clean and readable.

Handle Network Errors

Always wrap protobuf parsing in try-catch blocks, especially when reading from network or storage.

Common Issues

Issue: protoc_plugin not found

Solution: Make sure ~/.pub-cache/bin (or Windows equivalent) is in your PATH. Reactivate withdart pub global activate protoc_plugin.

Issue: Import errors in generated files

Solution: Make sure the protobuf package is added to your pubspec.yaml and run flutter pub get.

Issue: Hot reload doesn't pick up proto changes

Solution: After regenerating proto files, do a full restart (flutter run) instead of hot reload.

Related Tools

Additional Resources

Official Documentation & References

Conclusion

Protocol Buffers is an excellent choice for Flutter apps. The performance benefits are real: smaller app downloads, reduced bandwidth usage, faster data processing, and better battery life. Dart's clean syntax makes working with protobuf messages natural and enjoyable.

Start by using protobuf for your API communication and local caching. Your users will notice the difference in app responsiveness and data usage. The initial setup is worth the long-term benefits.