Android components and lifecycle — choosing correctly
Components are operating-system entry points
An Android application does not own one permanent entry point. Android can enter it through a declared Activity, Service, BroadcastReceiver, or ContentProvider. Each component tells the system why the process matters and what contract it exposes.
:::warning Android-only chapter Component, lifecycle, manifest, and WorkManager examples need an Android project and cannot run in Kotlin Playground. :::
The four component types
| Component | Responsibility | Real example | Wrong assumption |
|---|---|---|---|
| Activity | User-facing task/screen entry | Checkout screen | “An Activity is the entire app” |
| Service | Started/bound component for ongoing work or API | Music playback, bound device service | “Service means background thread” |
| BroadcastReceiver | Short response to delivered broadcast | Schedule work after boot | “Receiver can upload for minutes” |
| ContentProvider | URI-based structured data interface | Share documents/search suggestions | “Every Room database needs one” |
Component versus ordinary class
class PriceCalculator {
fun total(lines: List<Line>): Money = TODO()
}
You construct and call this class. Android constructs component instances and delivers lifecycle/IPC callbacks according to platform state.
class CheckoutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
Do not call CheckoutActivity() as navigation. Ask Android through an Intent/navigation API.
Manifest as installed contract
<application ...>
<activity
android:name=".CheckoutActivity"
android:exported="false" />
<receiver
android:name=".BootReceiver"
android:exported="true"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
The merged manifest tells Android which components exist, whether outsiders can reach them, required permissions, intent filters, processes, and other policy. Inspect the merged result because libraries contribute entries.
Service does not create a worker thread
class ExportService : Service() {
override fun onStartCommand(
intent: Intent?,
flags: Int,
startId: Int
): Int {
exportDatabaseBlocking() // Main-thread work: ANR risk.
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
}
onStartCommand() normally runs on the Service process’s main thread. A Service changes component/process importance and provides lifecycle; it does not supply concurrency.
Choose work mechanism from requirements
| Requirement | Starting choice |
|---|---|
| Request needed only while visible screen lives | Lifecycle-aware coroutine/ViewModel |
| Deferrable work that must eventually complete | WorkManager |
| User-noticeable continuous activity | Eligible foreground service with notification |
| Exact user-facing clock/calendar event | Alarm API only when exactness is justified/allowed |
| Cross-process callable API | Bound service/Binder/AIDL if a real process boundary exists |
Scenario: photo upload
- User taps upload and leaves app; upload should resume under constraints → WorkManager.
- User watches immediate upload progress and cancelling screen cancels upload → screen/ViewModel coroutine may suffice.
- User explicitly starts a long media transfer that must remain visible/ongoing → evaluate foreground-service rules and user notification.
Architecture begins with semantics, not whichever API name contains “background.”
Receiver callbacks are short
❌ Avoid
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
repository.synchronizeEverythingBlocking()
}
}
The callback has a short lifetime and runs on the main thread by default. The process can lose importance after it returns.
✅ Prefer enqueueing durable work
override fun onReceive(context: Context, intent: Intent) {
WorkManager.getInstance(context).enqueueUniqueWork(
"post-boot-sync",
ExistingWorkPolicy.KEEP,
OneTimeWorkRequestBuilder<SyncWorker>().build()
)
}
Unique work prevents repeated boot/package events from creating duplicate logical jobs.
ContentProvider is an API boundary
A provider is useful when data/resources must be queried or shared through URIs, possibly across apps/processes. It owns validation, permissions, URI grants, query contracts, and potentially concurrency.
Do not expose a provider merely because Room stores data. Internal repositories can use Room directly without publishing an IPC surface.
Lifecycle is usability, not immortality
An Activity in RESUMED is interactive. A Fragment view in STARTED is visible. These states guide resource ownership and collection. They do not promise the process will later receive a cleanup callback.
❌ Avoid correctness in onDestroy()
override fun onDestroy() {
saveCustomerOrder()
super.onDestroy()
}
✅ Prefer event/transaction-based persistence
Save when meaningful state changes or according to a debounced draft policy. Treat destruction as cleanup opportunity, not the only commit point.
Component exposure security
Every exported component is a device-local public endpoint.
- Validate all Intent/URI/extras.
- Require appropriate permissions.
- Authorize the requested domain operation.
- Reject oversized/unexpected data.
- Avoid leaking sensitive results.
- Test calls from an untrusted test app.
exported="false" is the safest default when no external caller is required.
Android components are OS-managed entry points, not ordinary objects. Activities represent user-facing task entries; Services provide started/bound lifecycle but no automatic worker thread; Receivers handle short broadcast work; Providers expose URI-based data APIs. I select components from lifetime and visibility requirements, keep callbacks short, persist important state before destruction, and treat every exported component as a secured public API.
Exercises
- Choose mechanisms for music playback, nightly logs, a screen API request, and a precise alarm-clock ring.
- Why does moving blocking code into a Service not prevent ANR?
- Threat-model an exported “approve payment” Receiver.