How to Use Protocol Buffers in Kotlin

A practical guide to working with Protocol Buffers in Kotlin and Android

Published: January 2025 • 8 min read

Kotlin and Protocol Buffers work excellently together, especially for Android development. Kotlin's concise syntax and null safety combine well with Protobuf's efficient serialization, making them ideal for mobile apps and backend services.

This guide shows you how to use Protocol Buffers in Kotlin projects, covering both Android and JVM applications.

What You'll Need

  • Kotlin: Version 1.6 or higher
  • Gradle: For dependency management
  • Protocol Buffer Compiler: We'll configure this with Gradle

Step 1: Configure Gradle

Update your build.gradle.kts:

plugins {
    kotlin("jvm") version "1.9.0"
    id("com.google.protobuf") version "0.9.4"
}

dependencies {
    implementation("com.google.protobuf:protobuf-kotlin:3.25.0")
    implementation("com.google.protobuf:protobuf-java:3.25.0")
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.25.0"
    }
    
    generateProtoTasks {
        all().forEach { task ->
            task.builtins {
                create("kotlin")
            }
        }
    }
}

For Android projects, use protobuf-javalite instead for smaller APK size.

Step 2: Set Up Project Structure

kotlin-protobuf-example/
├── build.gradle.kts
├── src/
│   └── main/
│       ├── proto/
│       │   └── person.proto
│       └── kotlin/
│           └── Main.kt

Step 3: Create Your .proto File

Create src/main/proto/person.proto:

syntax = "proto3";

package person;

option java_multiple_files = true;
option java_package = "com.example.person";

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;
  repeated string phone_numbers = 4;
  
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  
  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }
  
  repeated PhoneNumber phones = 5;
  bool is_active = 6;
}

message AddressBook {
  repeated Person people = 1;
}

Step 4: Use Protocol Buffers in Kotlin

Create src/main/kotlin/Main.kt:

import com.example.person.*
import java.io.File

fun main() {
    // Create a Person using Kotlin DSL
    val person = person {
        name = "John Doe"
        id = 1234
        email = "john.doe@example.com"
        phoneNumbers.addAll(listOf("555-1234", "555-5678"))
        phones.add(
            phoneNumber {
                number = "555-9999"
                type = Person.PhoneType.MOBILE
            }
        )
        isActive = true
    }

    println("Created Person:")
    println("Name: ${person.name}")
    println("ID: ${person.id}")
    println("Email: ${person.email}")

    // Serialize to bytes
    val bytes = person.toByteArray()
    println("\nSerialized to ${bytes.size} bytes")

    // Deserialize from bytes
    val decoded = Person.parseFrom(bytes)
    println("\nDeserialized Person:")
    println("Name: ${decoded.name}")
    println("Active: ${decoded.isActive}")

    // Save to file
    File("person.bin").writeBytes(bytes)
    println("\nSaved to person.bin")

    // Read from file
    val fileData = File("person.bin").readBytes()
    val fromFile = Person.parseFrom(fileData)
    println("Read from file: ${fromFile.name}")

    // Create AddressBook
    val addressBook = addressBook {
        people.add(person)
        people.add(
            person {
                name = "Jane Smith"
                id = 5678
                email = "jane@example.com"
                isActive = true
            }
        )
    }

    println("\nAddress book has ${addressBook.peopleCount} people")

    // Use when expression with sealed enum
    person.phonesList.forEach { phone ->
        val typeStr = when (phone.type) {
            Person.PhoneType.MOBILE -> "Mobile"
            Person.PhoneType.HOME -> "Home"
            Person.PhoneType.WORK -> "Work"
            else -> "Unknown"
        }
        println("Phone: ${phone.number} ($typeStr)")
    }
}

Step 5: Build and Run

./gradlew build
./gradlew run

Expected output:

Created Person:
Name: John Doe
ID: 1234
Email: john.doe@example.com

Serialized to 78 bytes

Deserialized Person:
Name: John Doe
Active: true

Saved to person.bin
Read from file: John Doe

Address book has 2 people

Kotlin-Specific Features

DSL Builders

val person = person {
    name = "John"
    id = 123
    // Concise, type-safe builder
}

Copy and Modify

val updated = person.copy {
    email = "newemail@example.com"
}

Null Safety

// Kotlin handles protobuf fields safely
val email: String = person.email // Never null
val hasEmail = person.email.isNotEmpty()

Extension Functions

fun Person.displayName() = "ID: $id - $name"

println(person.displayName())

Using in Android

For Android, use the Lite version for smaller APK size:

// In build.gradle.kts (app module)
dependencies {
    implementation("com.google.protobuf:protobuf-javalite:3.25.0")
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.25.0"
    }
    generateProtoTasks {
        all().forEach { task ->
            task.builtins {
                create("java") {
                    option("lite")
                }
                create("kotlin") {
                    option("lite")
                }
            }
        }
    }
}

Use with DataStore:

val dataStore = context.createDataStore(
    fileName = "person.pb",
    serializer = PersonSerializer
)

// Save
dataStore.updateData { person }

// Read
val personFlow: Flow<Person> = dataStore.data

Best Practices for Kotlin

Use DSL Builders

Kotlin DSL builders make protobuf code more readable and idiomatic. Always prefer them over Java-style builders.

Leverage Coroutines

Use suspend functions for I/O operations with protobuf data.

Use Lite for Android

Always use protobuf-javalite for Android apps to reduce APK size and improve performance.

Common Issues

Issue: Protobuf plugin not found

Solution: Add the plugin to your root build.gradle.kts:id("com.google.protobuf") version "0.9.4" apply false

Issue: Generated files not found

Solution: Run ./gradlew generateProto to generate code manually.

Issue: DSL builders not available

Solution: Make sure you included protobuf-kotlin dependency and enabled Kotlin generation in the protobuf plugin.

Related Tools

Conclusion

Protocol Buffers with Kotlin offers a great developer experience. The Kotlin DSL makes the code readable and concise, while Protobuf provides efficient serialization. This combination is especially powerful for Android development.

Whether you're building mobile apps or backend services, Kotlin and Protobuf give you type safety, performance, and modern language features all in one package.