🤖 Unit 5 • Android Core Architecture

Activities & Application Components

The definitive operational study of Android's component model: Activity lifecycle state transitions (onCreate through onDestroy), process death & state persistence, configuration changes, explicit & implicit Intent resolution, data transport via Bundles & Parcelable, the 4 core application components (Activities, Services, Broadcast Receivers, Content Providers), and Context memory management.

Curriculum: Mobile Software Engineering (Android Track)
Unit: 05 of 15
Prerequisite: Unit 03 (Project Structure), Unit 04 (UI Design)
API Target: Android 14 / 15 (API 34/35)
01

The 4 Core Application Components & System Server

🏛️ The Quad-Pillar Architecture of Android

Unlike standard desktop applications that possess a single, monolithic entry point (like public static void main), an Android application is built as a loose collection of independent entry points invoked by the Android OS Linux kernel and system server (via Binder IPC). Every component serves a specialized operational contract:

  • Activities: The single focused window presentation layer for user interaction. Governed by a strict state-driven lifecycle.
  • Services: Background execution workers running without a direct graphical UI. Handles long-running audio playback, periodic synchronization, and sensor polling.
  • Broadcast Receivers: Event-driven message listeners that wake the app in response to system-wide or app-specific broadcast announcements (e.g., battery low, airplane mode, custom push payloads).
  • Content Providers: Secure, inter-process data abstraction gateways managing structured database access (SQLite/Room) across application sandboxes via uniform content:// URIs.
Flowchart 1: Android System Server & Component Dispatch Architecture
Linux Kernel / Binder IPC Transaction Bus
↔
System Server ActivityTaskManager (ATMS)
↔
Zygote Pre-forked App Process Dalvik / ART Runtime
↓
Activity UI Window & Touch Events
Service Foreground / Background Ops
BroadcastReceiver System & Custom Events
ContentProvider Cross-App Data Sharing
02

The Activity Life Cycle: States & Callback Transitions

🔄 The 7 Lifecycle Callback Methods

As users navigate forward, press back, answer phone calls, or switch apps, the Android OS drives the Activity through a deterministic state machine:

  • onCreate(): Called when the Activity is first instantiated. This is where static initialization occurs: inflating layouts (setContentView), initializing ViewModels, and restoring saved instance state bundles.
  • onStart(): Called when the Activity becomes visible to the user, but not yet interactive. Useful for registering UI-observing listeners.
  • onResume(): Called when the Activity enters the Resumed (Active/Running) state. The Activity is in the foreground, has input focus, and receives user touch/keystroke events. Camera previews and animations start here.
  • onPause(): Called when the Activity is partially obscured (e.g., by a runtime permission dialog, semi-transparent activity, or multi-window split screen). The activity is visible but has lost input focus. Never execute slow database writes here!
  • onStop(): Called when the Activity is no longer visible (user pressed Home or navigated to another full-screen Activity). Heavy resources, hardware sensors, and location tracking must be released here.
  • onRestart(): Called when an activity in the Stopped state is being re-displayed before onStart().
  • onDestroy(): Final cleanup callback before the Activity object is destroyed by calling finish() or reclaimed by the OS to free memory.
Flowchart 2: The Activity Lifecycle State Machine
Activity Launched Intent Dispatched
→
onCreate() Inflate & Initialize
→
onStart() Visible (No Focus)
→
onResume() Active & Interactive
↓ Another Activity Launched / Screen Locked
onPause() Partially Obscured
→
onStop() Hidden from Screen
→
onDestroy() Reclaimed / Finished
↓ User Navigates Back to Stopped Activity
onRestart() Transition from Stopped
→
onStart() Re-rendered Visible
→
onResume() Regains Touch Focus
Activity State Visibility Status Input Focus OS Kill Priority Permitted Operations
Resumed (Running) 100% Foreground Visible Has Full User Focus Lowest (Killed only in extreme panic) Active rendering, real-time gesture input, video playback
Paused Partially Visible (Obscured) Lost Focus Low Pause audio, freeze animations, release camera capture
Stopped Completely Hidden (0% Visible) No Focus Moderate (Eligible for process kill) Persist draft data, unregister GPS/Bluetooth listeners
Destroyed Terminated / Removed None Killed & Evicted from Memory Cancel background coroutines, close DB connections
03

Configuration Changes, Process Death & State Persistence

📱 Configuration Changes (Rotation / Themes)

When device configuration changes (screen orientation rotation, system dark mode toggle, language swap, or multi-window resizing), Android completely destroys and recreates the active Activity:

onPause() → onStop() → onDestroy() → onCreate() → onStart() → onResume().

This ensures the app automatically loads alternative resource qualifiers (e.g., res/layout-land/ or res/values-night/).

💾 onSaveInstanceState(Bundle)

To prevent data loss during recreation or OS process death, Android provides state persistence hooks:

  • onSaveInstanceState(outState: Bundle): Writes transient UI state (e.g., scroll positions, unsubmitted form inputs) before the activity is destroyed.
  • savedInstanceState in onCreate(): Reads the persisted bundle upon recreation to restore UI state seamlessly.
LifecycleLoggingActivity.kt — State Persistence & Restoration
package com.devascent.lifecycle

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.devascent.databinding.ActivityLifecycleBinding

class LifecycleLoggingActivity : AppCompatActivity() {

    private lateinit var binding: ActivityLifecycleBinding
    private var timerSeconds: Int = 0
    private val tag = "ActivityLifecycle"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityLifecycleBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Restore transient state after configuration change / process kill
        if (savedInstanceState != null) {
            timerSeconds = savedInstanceState.getInt("KEY_TIMER", 0)
            Log.d(tag, "onCreate: Restored timer state = $timerSeconds")
        } else {
            Log.d(tag, "onCreate: Fresh activity initialization")
        }
    }

    override fun onStart() {
        super.onStart()
        Log.d(tag, "onStart: Activity becoming visible")
    }

    override fun onResume() {
        super.onResume()
        Log.d(tag, "onResume: Activity interactive and in foreground")
    }

    override fun onPause() {
        super.onPause()
        Log.d(tag, "onPause: Losing input focus")
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt("KEY_TIMER", timerSeconds)
        Log.d(tag, "onSaveInstanceState: Transient state saved to Bundle")
    }

    override fun onStop() {
        super.onStop()
        Log.d(tag, "onStop: Activity hidden from view")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(tag, "onDestroy: Activity resource teardown")
    }
}
04

Intents: Explicit, Implicit & Inter-Activity Data Transport

✉️ The Intent Messaging Subsystem

An Intent is an asynchronous messaging object used to request an action from another app component or third-party application. There are two fundamental intent types:

  • Explicit Intents: Specify the exact destination target component by its explicit class name (e.g., Intent(this, DetailActivity::class.java)). Typically used to navigate internally within your own application sandbox.
  • Implicit Intents: Declare a general action to perform (e.g., Intent.ACTION_VIEW, Intent.ACTION_SEND, Intent.ACTION_DIAL) alongside a URI data payload and MIME type. The Android OS queries all installed apps via their <intent-filter> declarations and displays the system App Chooser.
Flowchart 3: Android OS Implicit Intent Resolution Pipeline
App Dispatches Intent ACTION_VIEW + "https://..."
→
PackageManagerService Inspects all manifests
→
Intent Filter Match Action, Data, Category
↓
Single Match Direct Launch (e.g. Chrome)
↔
Multiple Matches System Resolver Dialog / App Chooser
↔
Zero Matches ActivityNotFoundException (Crash!)
📦 Passing Complex Data: Parcelable vs. Serializable

Primitive data types are bundled via intent.putExtra("key", value). For custom Kotlin objects, Android requires serialization:

  • Java Serializable: Reflection-based standard Java serialization. Extremely slow on Android due to extensive runtime garbage collection and object allocation overhead.
  • Android Parcelable: High-performance IPC serialization mechanism designed specifically for Android's Binder bus. Explicitly writes fields into a flattened binary memory chunk. Always use @Parcelize via the Kotlin Parcelize plugin!
IntentNavigation.kt — Explicit, Implicit & ActivityResultContracts
package com.devascent.navigation

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import kotlinx.parcelize.Parcelize

@Parcelize
data class StudentEnrollment(
    val studentId: String,
    val courseTitle: String,
    val tuitionFee: Double
) : Parcelable

class EnrollmentHostActivity : AppCompatActivity() {

    // 1. Modern Activity Result API (replaces deprecated startActivityForResult)
    private val paymentLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        if (result.resultCode == RESULT_OK) {
            val transactionId = result.data?.getStringExtra("KEY_TXN_ID")
            // Process confirmed enrollment
        }
    }

    fun launchExplicitCheckout() {
        val enrollment = StudentEnrollment("STU-882", "Android Mobile Track", 499.00)
        
        // Explicit Intent: Directly targets destination class
        val intent = Intent(this, PaymentCheckoutActivity::class.java).apply {
            putExtra("EXTRA_ENROLLMENT_DATA", enrollment)
        }
        paymentLauncher.launch(intent)
    }

    fun launchImplicitBrowser() {
        // Implicit Intent: System resolves web browser app
        val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://devascentacademy.me"))
        if (webIntent.resolveActivity(packageManager) != null) {
            startActivity(webIntent)
        }
    }
}
05

Services, Broadcast Receivers & Content Providers

⚙️ Services: Background Execution Models

A Service is an application component that executes background tasks without an interactive user interface:

  • Foreground Services: Perform operations noticeably perceived by the user (e.g., active GPS route navigation, media playback). Must post an ongoing, non-dismissible notification in the status bar so the user knows memory and battery are being consumed. Highly prioritized against OS termination.
  • Background Services: Operations invisible to the user. Strictly constrained since Android 8.0 (API 26); background services are terminated within minutes when the app leaves the foreground. Modern Android development uses WorkManager for reliable, deferred background tasks.
  • Bound Services: Implements a client-server interface (via bindService() and IBinder), allowing activities to interact, query, and perform IPC with the service.
📡 Broadcast Receivers

Enables apps to receive system or custom broadcast events:

  • Static Receivers: Registered in AndroidManifest.xml via <receiver>. Heavily restricted in modern Android to prevent apps waking up simultaneously on battery/network broadcasts.
  • Dynamic Receivers: Registered programmatically at runtime via registerReceiver() with an IntentFilter. Must be unregistered in onStop() or onDestroy() to prevent memory leaks.
🗄️ Content Providers & URIs

Manages shared structured data stores across application sandboxes:

  • Content URI Structure: Follows standard schema: content://com.devascent.provider/courses/101 (Scheme, Authority, Path, ID).
  • ContentResolver: Client apps query providers via contentResolver.query(), insert(), update(), and delete(), receiving standard tabular Cursor results.
06

Context Architecture: Activity Context vs. Application Context

⚠️ Preventing Catastrophic Context Memory Leaks

Context is the universal interface to global application environment information, file storage, asset loading, system services, and theme resolution. Passing the wrong context type is the #1 leading cause of memory leaks in Android apps:

  • Activity Context (this): Tied directly to the lifecycle of a single Activity window. Contains references to the UI view hierarchy, window decorations, and current theme styling. Never store an Activity context inside a static variable or long-lived singleton! Doing so retains the entire Activity and all inflated Views in memory after onDestroy(), causing huge memory leaks.
  • Application Context (applicationContext): Tied to the lifecycle of the overall application process. Survives across Activity recreations and configuration changes. Safe for singletons, database managers, and background workers. Cannot inflate themed dialogs!
Operation / Task Activity Context Application Context Critical Architectural Rule
Show AlertDialog / Toast ✅ YES (Required for Dialogs) ❌ NO (Throws BadTokenException) Dialogs require a valid Window Token present only in Activity
Inflate XML Layout ✅ YES (Applies custom Theme) ⚠️ Partial (Applies default system theme) Use Activity context to ensure correct color styling
Singleton / Database Manager ❌ NEVER (Severe Memory Leak) ✅ YES (Matches Process Lifespan) Singletons live forever; referencing an Activity leaks it
Register Dynamic BroadcastReceiver ✅ YES (Unregister in onStop) ✅ YES (For process-wide listeners) Always mirror register/unregister pairs precisely
07

Production Capstone: Dual-Activity Flow & Lifecycle Tracking

🚀 Complete Inter-Component Data Exchange Implementation

Below is a fully functional dual-activity workflow demonstrating Parcelable data bundling, explicit navigation via the modern ActivityResultContracts API, two-way result communication, and comprehensive lifecycle Logcat tracking.

CheckoutWorkflow.kt — Dual-Activity Order & Payment Processing
package com.devascent.checkout

import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.devascent.databinding.ActivityOrderBinding
import com.devascent.databinding.ActivityPaymentBinding
import kotlinx.parcelize.Parcelize

// 1. Parcelable Domain Model
@Parcelize
data class CourseOrder(
    val orderId: String,
    val courseTitle: String,
    val amount: Double
) : Parcelable

// 2. Primary Order Activity
class OrderActivity : AppCompatActivity() {

    private lateinit var binding: ActivityOrderBinding
    private val logTag = "DevAscentComponent"

    // Register Activity Result Contract for modern callback handling
    private val checkoutLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        if (result.resultCode == RESULT_OK) {
            val paymentRef = result.data?.getStringExtra("EXTRA_PAYMENT_REF") ?: "UNKNOWN"
            binding.statusLabel.text = "Enrollment Complete! Ref: $paymentRef"
            Log.i(logTag, "OrderActivity: Payment acknowledged with reference: $paymentRef")
        } else {
            binding.statusLabel.text = "Checkout Cancelled by Student"
            Log.w(logTag, "OrderActivity: Checkout aborted")
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityOrderBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.btnProceedCheckout.setOnClickListener {
            val order = CourseOrder("ORD-2026-99", "Unit 5: Android Components", 199.99)
            
            // Build Explicit Intent with Parcelable Payload
            val intent = Intent(this, PaymentActivity::class.java).apply {
                putExtra("EXTRA_ORDER_PARCEL", order)
            }
            Log.d(logTag, "OrderActivity: Launching PaymentActivity for result")
            checkoutLauncher.launch(intent)
        }
    }
}

// 3. Secondary Destination Activity
class PaymentActivity : AppCompatActivity() {

    private lateinit var binding: ActivityPaymentBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityPaymentBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Retrieve Parcelable object
        val order: CourseOrder? = intent.getParcelableExtra("EXTRA_ORDER_PARCEL")
        binding.txtCourseTitle.text = order?.courseTitle ?: "No Order Data"
        binding.txtTotalDue.text = "Total Due: $${order?.amount}"

        binding.btnConfirmPayment.setOnClickListener {
            // Return result intent to caller
            val resultIntent = Intent().apply {
                putExtra("EXTRA_PAYMENT_REF", "TXN-SUCCESS-7718")
            }
            setResult(RESULT_OK, resultIntent)
            finish() // Destroy activity and pop off backstack
        }
    }
}
Runtime Logcat & Component Lifecycle Stream
D/DevAscentComponent: OrderActivity.onCreate() -> Layout inflated & ActivityResultContract registered D/DevAscentComponent: OrderActivity.onStart() -> Visible in foreground D/DevAscentComponent: OrderActivity.onResume() -> Input focus acquired D/DevAscentComponent: OrderActivity: Launching PaymentActivity for result D/DevAscentComponent: OrderActivity.onPause() -> Lost focus to PaymentActivity D/DevAscentComponent: PaymentActivity.onCreate() -> Parcelable CourseOrder('ORD-2026-99') deserialized D/DevAscentComponent: PaymentActivity.onStart() -> Payment view visible D/DevAscentComponent: PaymentActivity.onResume() -> Payment interactive D/DevAscentComponent: OrderActivity.onStop() -> Hidden behind full-screen PaymentActivity D/DevAscentComponent: PaymentActivity -> btnConfirmPayment clicked -> setResult(RESULT_OK) -> finish() D/DevAscentComponent: PaymentActivity.onPause() -> Tearing down D/DevAscentComponent: OrderActivity.onRestart() -> Waking from Stopped state D/DevAscentComponent: OrderActivity.onStart() -> Becoming visible D/DevAscentComponent: OrderActivity.onResume() -> Regained focus I/DevAscentComponent: OrderActivity: Payment acknowledged with reference: TXN-SUCCESS-7718 D/DevAscentComponent: PaymentActivity.onDestroy() -> Completely evicted from backstack
08

University Exam & Technical Interview Viva Preparation Guide

Q1. What is the exact difference between onPause() and onStop()?
onPause() indicates the Activity has lost input focus but is still partially visible to the user (e.g., when a dialog, runtime permission prompt, or multi-window split screen overlays the activity). In contrast, onStop() is invoked when the Activity is completely hidden from view (e.g., a new full-screen activity was launched or the user pressed Home). CPU-intensive teardowns and memory releases belong in onStop(), never in onPause().
Q2. What occurs during an Android Configuration Change (e.g. Device Rotation)? How can state be preserved?
During a configuration change, the Android OS destroys and completely recreates the active Activity instance by executing: onPause() → onStop() → onDestroy() → onCreate() → onStart() → onResume(). This guarantees proper resource qualifier reloading (e.g., swapping layout-port/ for layout-land/). Transient UI state is preserved using onSaveInstanceState(outState: Bundle) or by retaining business logic in architectural Jetpack ViewModels, which survive configuration changes.
Q3. What is the difference between an Explicit Intent and an Implicit Intent?
An Explicit Intent specifies the exact target component class name (e.g., Intent(this, TargetActivity::class.java)), ensuring the intent resolves strictly to an internal application class. An Implicit Intent specifies an abstract action (e.g., ACTION_VIEW, ACTION_SEND) and a data URI/MIME type, asking the Android OS PackageManager to resolve any installed third-party app matching the criteria via declared <intent-filter> elements.
Q4. Why is Android Parcelable preferred over Java Serializable for inter-process communication (IPC)?
Standard Java Serializable is a marker interface that relies heavily on Java Reflection to serialize object graphs, creating hundreds of short-lived temporary objects and inducing severe Garbage Collection pauses. Android Parcelable is engineered explicitly for high-speed binary memory copying across the Linux Binder IPC boundary. Parcelable generates explicit bytecode serialization, achieving 10x–20x higher performance.
Q5. Why was startActivityForResult() deprecated, and what replaced it?
startActivityForResult(intent, requestCode) was tightly coupled to a single monolithic callback method (onActivityResult), requiring giant nested switch/case blocks for request codes. Furthermore, if the launching Activity was destroyed by the OS due to memory pressure while the child activity was open, the result was frequently dropped upon recreation. It was replaced by the Activity Result API (registerForActivityResult), which decouples result handling into isolated, type-safe, lifecycle-aware contracts that automatically re-register across process death.
Q6. What is a Foreground Service and why is a persistent status bar notification mandatory?
A Foreground Service performs user-noticeable background operations (e.g., music streaming, turn-by-turn navigation, active workout tracking). Android requires a persistent, non-dismissible notification (startForeground(id, notification)) to ensure complete transparency and user awareness that an application process is consuming device battery, CPU, or hardware sensors, and gives the process high OOM priority so the kernel does not kill it.
Q7. What causes an Activity Context memory leak, and when must applicationContext be used instead?
An Activity context maintains a strong reference to the entire View hierarchy, theme system, and window manager. If an Activity context is passed to a long-lived object (such as a static variable, singleton, or background repository), the Activity cannot be garbage collected when destroyed by the OS or user, leaking hundreds of megabytes of memory. applicationContext is bound to the lifespan of the OS process itself and must be used for singletons and repositories.
Q8. What is the purpose of a Content Provider in Android's component model?
Android enforces strict process sandboxing: by default, App A cannot access App B's internal SQLite database files. A Content Provider acts as a secure, structured IPC gateway that exposes data tables via uniform content:// URIs. It allows external applications to perform CRUD operations (via ContentResolver) while allowing the host app to enforce granular read/write permissions, SQL injection filtering, and relational schema encapsulation.