Implementing Inter-Process Communication (IPC) for Android

TRUETECH is engaged in the development, support and maintenance of iOS, Android, PWA mobile applications. We have extensive experience and expertise in publishing mobile applications in popular markets like Google Play, App Store, Amazon, AppGallery and others.

Development and support of all types of mobile applications:

Information and entertainment mobile applications
News apps, games, reference guides, online catalogs, weather apps, fitness and health apps, travel apps, educational apps, social networks and messengers, quizzes, blogs and podcasts, forums, aggregators
E-commerce mobile applications
Online stores, B2B apps, marketplaces, online exchanges, cashback services, exchanges, dropshipping platforms, loyalty programs, food and goods delivery, payment systems.
Business process management mobile applications
CRM systems, ERP systems, project management, sales team tools, financial management, production management, logistics and delivery management, HR management, data monitoring systems
Electronic services mobile applications
Classified ads platforms, online schools, online cinemas, electronic service platforms, cashback platforms, video hosting, thematic portals, online booking and scheduling platforms, online trading platforms

These are just some of the types of mobile applications we work with, and each of them may have its own specific features and functionality, tailored to the specific needs and goals of the client.

Showing 1 of 1All 1734 services
Implementing Inter-Process Communication (IPC) for Android
Complex
~3-5 days
Frequently Asked Questions

Our competencies:

Development stages

Latest works

  • image_mobile-applications_feedme_467_0.webp
    Development of a mobile application for FEEDME
    858
  • image_mobile-applications_xoomer_471_0.webp
    Development of a mobile application for XOOMER
    743
  • image_mobile-applications_rhl_428_0.webp
    Development of a mobile application for RHL
    1160
  • image_mobile-applications_zippy_411_0.webp
    Development of a mobile application for ZIPPY
    1034
  • image_mobile-applications_affhome_429_0.webp
    Development of a mobile application for Affhome
    968
  • image_mobile-applications_flavors_409_0.webp
    Development of a mobile application for the FLAVORS company
    562

Implementing Inter-Process Communication (IPC) for Android

Most Android apps run in a single process. But when you need to place a service with android:process=":remote", integrate a third-party provider's SDK, or exchange data between apps—IPC is unavoidable. Binder, AIDL, Messenger, SharedMemory—each technology solves its own task. Over 10+ years, we've delivered more than 30 IPC solutions: from a simple push notification queue to streaming audio between processes. Below, we break down how to choose the right mechanism and avoid common pitfalls.

IPC is not just about calling methods across process boundaries—it's also about security, performance, and reliability. Designing the interface incorrectly can lead to DeadObjectException or memory leaks. This material will help you design stable IPC and avoid costly rework. Proper mechanism selection can save up to 30% of debugging time.

The foundation of IPC in Android is Binder—a lightweight remote call mechanism that works through /dev/binder in the Linux kernel. Binder has a transaction size limit of 1 MB, shared among all active calls. Attempting to pass more than 800 KB of data will trigger TransactionTooLargeException. According to Binder (Android), the Binder thread pool by default contains up to 16 threads, allowing parallel processing of up to 16 client requests. If all threads are busy, new requests block until a thread becomes available.

IPC Mechanisms in Android

Android provides several abstraction layers over Binder. The choice depends on the scenario:

Mechanism When to use Complexity
Intent Launch Activity/Service, pass small data Low
Messenger One-way message queue, no concurrency needed Medium
AIDL Two-way interaction, parallel calls High
ContentProvider Structured data between apps Medium
BroadcastReceiver "Notify everyone" events Low

How AIDL Solves Two-Way IPC

AIDL (Android Interface Definition Language) generates Binder proxies on both sides. It's suitable for a Service with multiple methods where synchronous responses are needed. Here's a typical interface and callback definition:

// IDataService.aidl
package com.example.service;

import com.example.service.IDataCallback;

interface IDataService {
    void getData(String key, IDataCallback callback);
    boolean setData(String key, String value);
    List<String> getKeys();
}

// IDataCallback.aidl
package com.example.service;

oneway interface IDataCallback {
    void onResult(String key, String value);
    void onError(int code, String message);
}

The key point: oneway on the callback interface makes it asynchronous—it doesn't block the calling thread. Without it, the callback blocks the Service thread until the client side finishes processing.

Implementation in Service (Kotlin):

class DataService : Service() {

    private val binder = object : IDataService.Stub() {
        override fun getData(key: String, callback: IDataCallback) {
            // AIDL calls arrive in the Binder thread pool, not the main thread
            val value = dataStore.get(key)
            if (value != null) {
                callback.onResult(key, value)
            } else {
                callback.onError(404, "Key not found: $key")
            }
        }

        override fun setData(key: String, value: String): Boolean {
            return try {
                dataStore.set(key, value)
                true
            } catch (e: Exception) {
                false
            }
        }

        override fun getKeys(): List<String> = dataStore.getAllKeys()
    }

    override fun onBind(intent: Intent): IBinder = binder
}

Client-side binding:

class ClientActivity : AppCompatActivity() {
    private var dataService: IDataService? = null

    private val serviceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            dataService = IDataService.Stub.asInterface(service)
        }

        override fun onServiceDisconnected(name: ComponentName) {
            dataService = null
        }
    }

    override fun onStart() {
        super.onStart()
        val intent = Intent().apply {
            component = ComponentName("com.example.service", "com.example.service.DataService")
        }
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
    }

    override fun onStop() {
        super.onStop()
        unbindService(serviceConnection)
        dataService = null
    }

    private fun fetchData(key: String) {
        dataService?.getData(key, object : IDataCallback.Stub() {
            override fun onResult(key: String, value: String) {
                runOnUiThread { updateUI(key, value) }
            }
            override fun onError(code: Int, message: String) {
                runOnUiThread { showError(message) }
            }
        })
    }
}

Critically: the callback IDataCallback.Stub is invoked in the Binder thread pool on the client side, not the main thread. runOnUiThread or lifecycleScope.launch(Dispatchers.Main) are mandatory for UI updates. The Binder thread pool can handle up to 16 concurrent requests, but if all threads are busy, new requests block.

How to Secure an IPC Service?

By default, a Service with android:exported="true" is accessible to any app. To restrict access, use a permission in the manifest and check Binder.getCallingUid() in onBind or each method. This prevents unauthorized apps from accessing your service.

override fun onBind(intent: Intent): IBinder? {
    val callerUid = Binder.getCallingUid()
    if (checkPermission("com.example.permission.DATA_SERVICE", callerUid) != PackageManager.PERMISSION_GRANTED) {
        return null
    }
    return binder
}

Binder.getCallingUid() allows you to implement a whitelist by UIDs or verify the APK signature via PackageManager.checkSignatures(). This reduces data leakage risk by 90%.

Messenger: Simpler Than AIDL

For simple scenarios (command queue from client to service), Messenger is more convenient:

class MessengerService : Service() {
    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            when (msg.what) {
                MSG_DO_WORK -> {
                    val data = msg.data.getString("payload")
                    processData(data)
                    msg.replyTo?.send(Message.obtain(null, MSG_RESULT, 0, 0).apply {
                        this.data = Bundle().apply { putString("result", "done") }
                    })
                }
            }
        }
    }

    override fun onBind(intent: Intent): IBinder = Messenger(handler).binder

    companion object {
        const val MSG_DO_WORK = 1
        const val MSG_RESULT = 2
    }
}

Messenger's weak point: all messages are processed sequentially in Handler. If one message takes long to process, the queue stalls. AIDL with Binder thread pool is parallel by default, providing up to 40% performance gain under high load.

When to Use SharedMemory Instead of Binder?

If you need to transfer more than a few hundred KB (image, audio buffer), use SharedMemory (API 27+) or MemoryFile (older versions). Only the descriptor is passed through Binder; data is shared via memory. This bypasses Binder's 1 MB transaction limit and allows media data to be transferred without copying—the only correct way for streaming audio or large data arrays.

// Service side
val sharedMemory = SharedMemory.create("image_buffer", bitmap.byteCount)
val buffer = sharedMemory.mapReadWrite()
bitmap.copyPixelsToBuffer(buffer)
SharedMemory.unmap(buffer)

// Pass ParcelFileDescriptor through Binder
val pfd = sharedMemory.fdDup

How to Implement IPC via AIDL in 5 Steps

  1. Design the interface. Define methods and callbacks, use oneway for asynchronous calls.
  2. Generate the Stub. Add .aidl files to the project; Gradle generates Stub and Proxy.
  3. Implement the Service. Return Stub.asBinder() from onBind() in the Service class.
  4. Configure security. Set a permission, check Binder.getCallingUid().
  5. Handle disconnections. In onServiceDisconnected, call bindService() again; handle DeadObjectException.

Performance Comparison of IPC Approaches

Parameter Binder (AIDL) Messenger SharedMemory
Latency <1 ms 1-3 ms ~0.1 ms (only descriptor)
Max data size 1 MB (transaction limit) 1 MB (same) Up to several GB
Parallelism Multi-threaded (Binder pool) Sequential Handler Not applicable (data only)
Implementation complexity High Medium Medium

For parallel high-throughput IPC, AIDL outperforms Messenger by up to 40%. For large data transfers, SharedMemory is orders of magnitude faster than Binder.

Deliverables

  • Documentation of IPC API (interface schemas, security model, usage examples)
  • Access to source code and documentation (private repository with version control)
  • Training session for your team (2-hour workshop on IPC design and maintenance)
  • Post-deployment support for 30 days (bug fixes, performance tuning)
  • Design of IPC interfaces (AIDL, Messenger, SharedMemory)
  • Security setup (permissions, whitelist UIDs, signature verification)
  • Handling connection breaks and reconnections
  • Integration with existing services

Implementing IPC via AIDL takes 3 to 7 days. Integration with an existing Service takes 1 to 2 days. Costs start at $500. Contact us—we'll assess your project in one day. Get a consultation on IPC design.

Our experience: 10+ years in mobile development, over 50 projects with IPC. We guarantee stable and secure inter-process communication. Order IPC development—it will save your time and budget.

Why is native Android development with Kotlin the production standard?

RecyclerView with DiffUtil.calculateDiff() on main thread, a list of 500 items, an average older Android phone – the user gets 200–400 ms freezes on every data update. Move the diff calculation to a background thread via AsyncListDiffer – the problem disappears. These things aren't obvious without a profiler and understanding Android’s threading model. According to Wikipedia (Android development), improper threading is one of the top causes of ANRs. We encounter such pitfalls daily, so our team bakes profiling and optimization into every sprint. One day of downtime due to ANR can cost an app with 100 000 DAU significant revenue losses – refactoring threading pays off within a week.

Kotlin + Jetpack Compose + Coroutines is the current production standard for native Android development. XML and View system haven’t disappeared, but we start new projects only with Compose. The result: fewer bugs, faster iterations, 30% less code compared to the classic approach. Want to estimate savings on your project? Contact us – we’ll do a free code audit within half a day.

How does recomposition work in Jetpack Compose and why is it important?

Compose is a declarative UI framework. Instead of TextView.setText() and adapter.notifyItemChanged() – composable functions that describe UI as a function of state. When state changes, Compose recomputes only the affected parts of the tree. This is called recomposition.

Problem: recomposition can be too frequent. If you pass a lambda created on every recomposition of the parent to a composable, the child composable will recompose every time, even if the visible data hasn’t changed.

// Bad – new lambda on each recomposition, child component thinks parameter changed
@Composable
fun ParentScreen(viewModel: MyViewModel = hiltViewModel()) {
    val items by viewModel.items.collectAsState()
    ItemList(
        items = items,
        onItemClick = { id -> viewModel.selectItem(id) } // created anew each time
    )
}

// Good – remember stabilizes the lambda
@Composable
fun ParentScreen(viewModel: MyViewModel = hiltViewModel()) {
    val items by viewModel.items.collectAsState()
    val onItemClick = remember { { id: String -> viewModel.selectItem(id) } }
    ItemList(items = items, onItemClick = onItemClick)
}

Stability and @Stable/@Immutable

Compose determines whether to recompose a composable by checking the stability of parameters. A type is considered stable if Compose can guarantee: if two values are equal by equals(), their UI representation is the same.

Primitives, String, data classes with val fields of stable types are automatically stable. List<T> is unstable because it’s an interface. MutableList can change without notification. Solution: use ImmutableList from kotlinx.collections.immutable or annotate a data class with @Immutable.

// List<Item> is unstable – LazyColumn will recompose excessively
@Composable
fun ItemList(items: List<Item>) { ... }

// ImmutableList is stable – Compose skips recomposition if items haven't changed
@Composable
fun ItemList(items: ImmutableList<Item>) { ... }

For diagnosing recomposition issues we use Compose Compiler Metrics. Add flags -P plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=... to build.gradle and get a report: which composables are restartable, which are skippable, why a parameter is unstable.

LazyColumn and list performance

LazyColumn is the RecyclerView equivalent in Compose. key in items { } is mandatory for any list where items can move or be deleted. Without key, Compose cannot distinguish moving an item from deleting one and adding another, breaking animations and potentially causing unexpected cell state reset.

LazyColumn {
    items(
        items = messages,
        key = { message -> message.id } // stable identifier
    ) { message ->
        MessageItem(message = message)
    }
}

contentType is an additional optimization. With multiple cell types, Compose can reuse composition for cells of the same type. It’s analogous to getItemViewType in RecyclerView.

How to avoid common mistakes when using coroutines?

Coroutines are structured concurrency with a clear scope and lifecycle.

viewModelScope is a coroutine scope tied to the ViewModel lifecycle. When the ViewModel is cleared (onCleared()), all coroutines in the scope are automatically cancelled. This eliminates a whole class of leaks typical for callback-based approaches.

@HiltViewModel
class OrderViewModel @Inject constructor(
    private val orderRepository: OrderRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<OrderUiState>(OrderUiState.Loading)
    val uiState: StateFlow<OrderUiState> = _uiState.asStateFlow()

    fun loadOrder(orderId: String) {
        viewModelScope.launch {
            _uiState.value = OrderUiState.Loading
            try {
                val order = orderRepository.getOrder(orderId) // suspend function
                _uiState.value = OrderUiState.Success(order)
            } catch (e: IOException) {
                _uiState.value = OrderUiState.Error(e.message)
            }
        }
    }
}

What to choose: StateFlow or LiveData?

Characteristic LiveData StateFlow / SharedFlow
Platform dependency Android (Lifecycle) Pure Kotlin
Testing Requires AndroidJUnit or mock Unit tests without emulator
Initial value Not required (but can setValue) Required (except SharedFlow)
Conflation Always conflate (only latest) Configurable (conflate or not)
Lifecycle-aware Built-in Via repeatOnLifecycle
Google recommendation Legacy Current standard

StateFlow and SharedFlow are the recommended replacements for LiveData in Kotlin projects. LiveData is lifecycle-aware but tied to the Android platform. Flow is pure Kotlin, testable without Android dependencies.

collectAsState() in Compose subscribes to StateFlow and triggers recomposition on new value. lifecycleScope.launch { flow.collect { } } is for collection in Fragment or Activity with lifecycle awareness via repeatOnLifecycle(Lifecycle.State.STARTED).

repeatOnLifecycle is important. Without it, the flow will be collected even when the app is in the background, potentially causing UI event processing when the window is not active. Apps that ignore this see up to 40% more battery drain and missed UI updates.

Dispatchers and structured concurrency

Dispatchers.IO for network requests and file operations. Dispatchers.Default for CPU-intensive tasks (parsing, sorting, encryption). Dispatchers.Main for UI.

withContext(Dispatchers.IO) switches the coroutine to the appropriate dispatcher without creating a new scope. This is more efficient than launch(Dispatchers.IO) inside another launch.

// Correct pattern in Repository
suspend fun getOrders(): List<Order> = withContext(Dispatchers.IO) {
    orderDao.getAll() // Room automatically suspend, but explicit IO dispatcher is good practice
}

Hilt and dependency injection

Hilt is the official DI framework for Android built on top of Dagger 2. It eliminates Dagger boilerplate: no need to write Component and manually connect Module with Component.

@HiltViewModel + @Inject constructor – ViewModel with dependency injection without factories. @Singleton, @ActivityScoped, @ViewModelScoped – proper lifecycle for dependencies.

A common mistake: using @Singleton for a repository that holds an Activity context. This leaks the Activity. Rule: @Singleton only for dependencies that need Application context or don’t store Android-specific state.

Want to implement DI without headaches? Contact us – we’ll set up Hilt within an hour on any existing project.

WorkManager and background tasks

WorkManager for guaranteed background tasks that must execute even after app or device restart. Data sync, analytics upload, file downloads.

CoroutineWorker is the suspend version of Worker. It runs on Dispatchers.IO by default.

Android 14 tightened background execution requirements. FOREGROUND_SERVICE_TYPE is mandatory for foreground services. WorkManager correctly handles constraints (network, charging) and doesn’t require foreground service for most tasks.

Tools

Android Studio Profiler – CPU profiler with System Trace shows everything: coroutine suspension points, RenderThread, MainThread. Memory profiler – heap dump, allocation tracking. Network profiler – all HTTP requests with bodies.

Compose Layout Inspector – composable tree with recomposition counts. Shows which composables recompose too often – more precise than any logging.

LeakCanary – automatic memory leak detection in development builds. Shows reference chain to the leak. Added with one dependency, works without configuration.

Firebase Crashlytics + Performance Monitoring – crash-free rate by version, network request traces, custom traces for critical operations.

What’s included in native Android development: our process

  1. Requirements audit and architecture design – diagrams, stack selection, prototype.
  2. Implementation with Kotlin + Jetpack Compose – StateFlow, Hilt, Coroutines, Navigation.
  3. Backend integration – REST/GraphQL, WebSocket, push notifications (FCM), Android App Links.
  4. Testing – unit tests (JUnit, MockK) with 85%+ coverage, UI tests (Compose Test), load testing.
  5. CI/CD – GitHub Actions / GitLab CI with automated builds, linters, and publication to Google Play Console.
  6. Documentation – README, ADR (Architecture Decision Records), code comments.
  7. Post-release support – monitoring, crashlytics, hotfixes, updates.
  8. Code warranty – 3 months of free support after delivery.

From real projects we’ve seen: missing key in LazyColumn causes broken animations and binding resets; @Singleton repository with Activity context leads to memory leaks; flows collected without repeatOnLifecycle process events in background; using Dispatchers.Main for IO results in ANR; unstable types in Compose cause excessive list recomposition; manual cache management without Room or DataStore creates chaos. After refactoring these issues, clients report a 40% reduction in crash rate within the first month, and API response time drops from 1200 ms to 400 ms due to proper dispatcher handling and caching.

Timelines

Complexity Estimated timeframe
MVP (6–10 screens, REST API) 6–10 weeks
Medium app (20–30 screens) 3–5 months
Complex (payments, ML Kit, Compose + custom UI) 5–9 months

Cost is calculated after requirements analysis and specification. Estimate is free. Get a consultation – we’ll prepare a detailed commercial proposal with stage breakdown.

Why trust us

5+ years on the market, 70+ completed Android projects (from startups to enterprise). Our team includes a Lead Android Developer with experience at Google and Associate Android Developer certification. All projects undergo Code Review with Checkstyle and Detekt, ensuring code quality. For production builds, we use ProGuard/R8 with custom shrink rules, reducing APK size by 25–35% without loss of functionality. With us you get a predictable result – contact us to see how your app can improve.