Skip to main content

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​

ComponentResponsibilityReal exampleWrong assumption
ActivityUser-facing task/screen entryCheckout screen“An Activity is the entire app”
ServiceStarted/bound component for ongoing work or APIMusic playback, bound device service“Service means background thread”
BroadcastReceiverShort response to delivered broadcastSchedule work after boot“Receiver can upload for minutes”
ContentProviderURI-based structured data interfaceShare 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​

RequirementStarting choice
Request needed only while visible screen livesLifecycle-aware coroutine/ViewModel
Deferrable work that must eventually completeWorkManager
User-noticeable continuous activityEligible foreground service with notification
Exact user-facing clock/calendar eventAlarm API only when exactness is justified/allowed
Cross-process callable APIBound 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.

Interview-ready answer

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​

  1. Choose mechanisms for music playback, nightly logs, a screen API request, and a precise alarm-clock ring.
  2. Why does moving blocking code into a Service not prevent ANR?
  3. Threat-model an exported “approve payment” Receiver.