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. If you're new to Protocol Buffers, check out our Protobuf vs JSON comparison first.

What You'll Need

Step 1: Configure Gradle

Update your build.gradle.kts with the protobuf-gradle-plugin:

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. See the Android DataStore documentation for more details.

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. Learn more about proto3 syntax in the official language guide:

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. The Kotlin DSL provides a more idiomatic way to work with protobuf compared to Java's builder pattern:

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

fun main() {
    // Create a Person using Kotlin DSL
    val person = person {
        name = "Maria Garcia"
        id = 1234
        email = "[email protected]"
        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 = "[email protected]"
                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: Maria Garcia
ID: 1234
Email: [email protected]

Serialized to 78 bytes

Deserialized Person:
Name: Maria Garcia
Active: true

Saved to person.bin
Read from file: Maria Garcia

Address book has 2 people

Kotlin-Specific Features

The protobuf-kotlin library provides several Kotlin-friendly features:

DSL Builders

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

Copy and Modify

val updated = person.copy {
    email = "[email protected]"
}

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. Learn more in the Android DataStore guide:

// 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 for type-safe persistent storage:

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

// Save
dataStore.updateData { person }

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

This is much more efficient than using JSON with Kotlin data classes for Android app data.

Best Practices for Kotlin

Follow these Protobuf API best practices and Kotlin-specific guidelines:

Use DSL Builders

Kotlin DSL builders make protobuf code more readable and idiomatic. Always prefer them over Java-style builders. See more in our best practices guide.

Leverage Coroutines

Use suspend functions for I/O operations with protobuf data. Learn more about Kotlin coroutines.

Use Lite for Android

Always use protobuf-javalite for Android apps to reduce APK size and improve performance. Check out our performance optimization guide.

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.

Want to explore more? Try our JSON to Protobuf converter to convert existing data schemas, or read about using Protobuf with gRPC for building efficient APIs.