Zygote
Builder trackintermediate

I01 — AIDL calculator

Two processes, one interface: build a calculator service with AIDL, bind to it from a client, and look at the generated Stub and Proxy to see what a Binder call actually does.

3 minAPI 21–37 · 17verified 2026-10-27

AIDL is where Binder becomes something you can read. You write an interface file, the build generates a Stub and a Proxy, and the two halves of a cross-process call appear in your own project.

What you are building#

Two apps in one project: a service app that exposes an add and a multiply method, and a client app that binds to it and calls them. The service runs in its own process, so the calls really do cross a Binder boundary.

Steps#

  1. Create the interface

    Create an .aidl file declaring int add(int a, int b) and int multiply(int a, int b). Put it in the source set that both the service and the client can see; in a two-module project, that is a shared module.1

    You should seeThe project generates a Java interface from it.
  2. The interface file is small:

    // IAddService.aidl
    package com.example.aidlservice;
     
    interface IAddService {
        int add(int a, int b);
        int multiply(int a, int b);
    }
  3. Add a parcelable type

    Add a simple Parcelable result class and an .aidl declaration for it. This is where you learn that AIDL only understands primitives, strings, lists, maps and parcelables.

    You should seeA second generated type.
  4. Implement the service

    Extend the generated Stub and implement the two methods. Return a computed value. Log the calling UID so you can see which process made the call.

    You should seeThe service compiles with a generated Stub.
  5. Declare the service with an intent filter

    Add the service to the manifest with android:exported="true" and an intent filter, or expose it by explicit component name. For a lab, explicit is simpler and safer.

    You should seeThe service is discoverable by name.
  6. The service side implements the generated Stub:

    class AddService : Service() {
        private val binder = object : IAddService.Stub() {
            override fun add(a: Int, b: Int): Int = a + b
            override fun multiply(a: Int, b: Int): Int = a * b
        }
     
        override fun onBind(intent: Intent): IBinder = binder
    }
  7. Bind from the client

    Bind with an explicit intent and a ServiceConnection. In onServiceConnected, cast the binder to the generated IAddService.Stub.asInterface type.

    You should seeonServiceConnected fires.
  8. Call across the boundary

    Call add and log the result. Then check the client's logcat for the service's log line: the two PIDs are different, which is the proof that the call crossed a process boundary.

    You should seeThe result comes back from the other process.
  9. Read the generated code

    Open the generated files and find asInterface, asBinder, transact and onTransact. The Proxy writes the method code and the arguments into a Parcel; the Stub reads them and calls your implementation.

    You should seeA Proxy that parcels and a Stub that unparcels.
  10. Add a oneway method

    Add a oneway void log(String message) and observe that the client does not wait. That is the difference between a synchronous transaction and a queued one.

    You should seeThe call returns immediately.
Prove the process boundary
adb shell ps -A | grep -E "aidlservice|aidlclient"
adb logcat -s AidlService:AidlClient
adb shell dumpsys activity services com.example.aidlservice
Example output
Illustrative — PIDs and package names differ.

Verification#

The two PIDs in the logs should differ. The service's log line should appear after the client's call, with the client's UID as the caller. If both PIDs are the same, the service is running in the client's process: check the android:process attribute and the binding.

What you learned#

  • AIDL is the declaration, the generated code is the mechanism. The Stub/Proxy pair is the Binder call in two halves.
  • A bound service is a client-server relationship, not a background task. The binding determines the lifetime.
  • Synchronous and oneway calls have different shapes and different failure modes.
  • A process boundary is observable. Two PIDs and a log line are enough to prove it.

Read AIDL for the type rules, and Binder overview for why the transaction is designed this way.

Extend it#

  1. Add a callback interface so the service can call back into the client, and handle the case where the client dies.
  2. Add a large payload and observe the transaction size limit; then move the payload to a file descriptor or a shared memory buffer.
  3. Add a permission to the service declaration and watch the call fail without it.

Sources#

Sources

Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.

  1. docsAndroid Interface Definition Language (AIDL) | Background workdeveloper.android.comaccessed 2026-10-27↩
  2. docsBinder overviewsource.android.comaccessed 2026-10-27↩
  3. docsServices overview | Background workdeveloper.android.comaccessed 2026-10-27↩
  4. docsApp manifest overviewdeveloper.android.comaccessed 2026-10-27↩
  5. docsBinder threadingsource.android.comaccessed 2026-10-27↩