Chapter 4: Core App Components

Introduction: The 4 Pillars of an Android App

An Android app is not just one single "program" that runs. Instead, it's a collection of independent, loosely-coupled components. The Android operating system itself is responsible for starting, stopping, and managing these components based on user actions and system events.

These components are the fundamental building blocks of any Android application. They are declared in a single, critical file we learned about in Chapter 2: the AndroidManifest.xml.

There are four main types of application components:

  1. Activities: The "UI" component. An activity is a single screen that the user can interact with. Your app is usually a collection of activities (e.g., MainActivity, ProfileActivity, SettingsActivity).
  2. Services: The "Background" component. A service runs in the background to perform long-running tasks *without* a user interface (e.g., playing music, downloading a file, or syncing data).
  3. Broadcast Receivers: The "Listener" component. A broadcast receiver's only job is to listen for and respond to system-wide messages (broadcasts), such as "the phone has finished booting" or "the charger has been connected."
  4. Content Providers: The "Data" component. A content provider manages a shared set of app data. It's the *only* way to securely share your app's data with other applications (e.g., the Contacts app uses a content provider to share your contact list with WhatsApp or Google Maps).

How do these components talk to each other? They use a "message" object called an **Intent**. An Intent is the "glue" that binds these components together. This chapter will cover each of these pillars in extreme detail.

Deep Dive: Activities and The Activity Lifecycle

An **Activity** is a class that represents a single, focused screen in your application. It's the most common and most important component you will build. MainActivity.kt, which is created with every new project, is an Activity.

The Activity's job is to draw the User Interface (using Jetpack Compose or XML, as we saw in Chapter 3) and to handle all user interaction for that screen (like button clicks or touch gestures).

Unlike a simple desktop program where you control the main() function, in Android, you *do not* control when your Activity starts or stops. The **Android Operating System** is in control. The user (by opening or closing the app) and the system (by receiving a phone call or running low on memory) can start, pause, resume, and destroy your Activity at any time.

To manage this, your Activity must "listen" to the OS by implementing a series of **lifecycle callback methods**.

The Activity Lifecycle (The Most Critical Concept)

The Activity Lifecycle is a set of states an Activity can be in during its entire life, from the moment it's created until it's destroyed. The Android system notifies your Activity of these state changes by calling specific callback methods. **Your job is to override these methods** to add your own logic.

Let's write a "spy" app to see exactly when these methods are called. We'll override every lifecycle method in MainActivity.kt and add a Logcat message to each.

// MainActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivityLifecycle"
    }

    // 1. CALLED FIRST: The Activity is being CREATED.
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG, "onCreate: Activity is being created.")
    }

    // 2. CALLED SECOND: The Activity is about to become VISIBLE.
    override fun onStart() {
        super.onStart()
        Log.d(TAG, "onStart: Activity is now visible.")
    }

    // 3. CALLED THIRD: The Activity is in the FOREGROUND and INTERACTIVE.
    override fun onResume() {
        super.onResume()
        Log.d(TAG, "onResume: Activity is in foreground and interactive.")
    }

    // 4. CALLED when user leaves: The Activity is PARTIALLY VISIBLE (e.g., dialog box)
    override fun onPause() {
        super.onPause()
        Log.d(TAG, "onPause: Activity is partially visible. Time to save data.")
    }

    // 5. CALLED when app is in background: The Activity is INVISIBLE.
    override fun onStop() {
        super.onStop()
        Log.d(TAG, "onStop: Activity is invisible (in background).")
    }

    // 6. CALLED when user navigates back: Called after onStop()
    override fun onRestart() {
        super.onRestart()
        Log.d(TAG, "onRestart: Activity is restarting.")
    }

    // 7. CALLED LAST: The Activity is being DESTROYED.
    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "onDestroy: Activity is being destroyed.")
    }
}

Lifecycle States Explained (What to do in each method)

onCreate(savedInstanceState: Bundle?)

  • When is it called? Exactly *once* when your Activity is first created.
  • What to do here: This is where you do all your one-time setup.
    • Call setContentView() (in XML) or setContent { } (in Compose) to show your UI.
    • Initialize your ViewModels.
    • Set up click listeners.
    • Restore saved state from the savedInstanceState bundle (more on this later).

onStart()

  • When is it called? When the Activity is about to become visible to the user.
  • What to do here: You can start animations or register any listeners (like a Broadcast Receiver) that should only run when the app is visible.

onResume()

  • When is it called? When the Activity is in the foreground and the user can interact with it. This is the "running" state.
  • What to do here: Start any services that need to run *only* when the user is actively using the app (e.g., start camera preview, start location updates).

onPause()

  • When is it called? When the Activity is about to lose focus. It's partially visible, but not interactive (e.g., a phone call comes in, or a semi-transparent dialog appears).
  • What to do here: This is your **last chance to save critical data**. This method must be *very fast*.
    • Save any unsaved data (like a draft in a text editor) to the database.
    • Stop resource-heavy tasks (like camera preview or sensor listeners).

onStop()

  • When is it called? When the Activity is no longer visible to the user (e.g., the user pressed the Home button or switched to another app).
  • What to do here: Release any resources you don't need while in the background. Stop animations, disconnect from network sockets.

onRestart()

  • When is it called? When the user navigates *back* to your app after it was in the "Stopped" state (e.g., they press the app icon again). It is always followed by onStart().

onDestroy()

  • When is it called? The Activity is being permanently destroyed. This happens for two reasons:
    1. The user manually finished it (e.g., by pressing the Back button).
    2. The system is temporarily destroying it due to a **Configuration Change** (like rotating the phone) or to save memory.
  • What to do here: Clean up any final resources (like closing database connections or unbinding services).

Example Scenarios:

  1. App Launch: onCreate() -> onStart() -> onResume(). (App is running)
  2. User presses Home button: onPause() -> onStop(). (App is in background)
  3. **User returns to app: onRestart() -> onStart() -> onResume(). (App is running again)
  4. **User presses Back button: onPause() -> onStop() -> onDestroy(). (App is finished)

Configuration Changes & State Handling

This is a classic "beginner trap" in Android. **By default, when a "configuration change" happens (like rotating the phone from portrait to landscape), Android *destroys* your entire Activity and *re-creates* it from scratch.**

onPause() -> onStop() -> onDestroy() -> onCreate() -> onStart() -> onResume()

If you have a counter variable in your Activity, it will be reset to 0! This is terrible UX.

The Old Way: `onSaveInstanceState`

To fix this, you override onSaveInstanceState. The system calls this method *before* destroying your Activity (after onPause()), giving you a chance to save small amounts of data (like the counter) into a "Bundle".

class MainActivity : AppCompatActivity() {
    var counter = 0
    val COUNTER_KEY = "MY_COUNTER"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 2. Restore the state if it exists
        if (savedInstanceState != null) {
            counter = savedInstanceState.getInt(COUNTER_KEY, 0)
        }
        
        // ... your setContentView and click listeners ...
        myButton.setOnClickListener { counter++ }
    }

    // 1. Save the state before destruction
    override fun onSaveInstanceState(outState: Bundle) {
        outState.putInt(COUNTER_KEY, counter)
        super.onSaveInstanceState(outState)
    }
}

The Modern Way: `ViewModel` (The Right Way)

The `onSaveInstanceState` method is only for small amounts of data. The **correct, modern** way to handle this is to *not* store state in the Activity at all. You store it in a special class called a **`ViewModel`**.

A `ViewModel` is a class designed to survive configuration changes. When the Activity is destroyed and re-created, the *same ViewModel instance* is re-connected to the new Activity, so your data is never lost. (We will cover this in **Chapter 7: Modern Architecture**).

Read More about the Activity Lifecycle →

Deep Dive: Intents (The "Glue")

An **Intent** is an asynchronous "message" that your app uses to request an action from another component. You don't start an Activity or Service by calling its class directly. Instead, you create an Intent object that *describes* what you want to do, and you pass that Intent to the Android system (e.g., startActivity(myIntent)). The system then finds the right component to handle it.

There are two types of Intents:

1. Explicit Intents

An Explicit Intent is "explicit" because you **name the exact component** (class) you want to start. You almost always use this to start components *within your own app*.

Example: Starting another Activity

// Inside MainActivity.kt
val goToProfileButton = findViewById<Button>(R.id.profile_button)

goToProfileButton.setOnClickListener {
    // Create an explicit intent for ProfileActivity
    val intent = Intent(this, ProfileActivity::class.java)
    
    // (Optional) Add extra data (like a "key-value" pair)
    intent.putExtra("USER_ID", 12345)
    intent.putExtra("USERNAME", "MSMAXPRO")
    
    // Start the new activity
    startActivity(intent)
}

// Inside ProfileActivity.kt, in onCreate()
val userId = intent.getIntExtra("USER_ID", 0) // 0 is a default value
val username = intent.getStringExtra("USERNAME")

2. Implicit Intents

An Implicit Intent is "implicit" because you **do not name the component**. Instead, you describe the **action** you want to perform (e.g., "open a webpage," "share text," "take a picture"). The Android system then finds *all* the apps on the user's phone that can handle this action (e.g., Chrome, Firefox, Opera for a webpage) and shows the user a "chooser" dialog.

Example 1: Opening a Webpage

val openWebButton = findViewById<Button>(R.id.web_button)
openWebButton.setOnClickListener {
    // 1. Define the Action (what to do)
    val action = Intent.ACTION_VIEW
    
    // 2. Define the Data (what to do it on)
    val data = Uri.parse("https://codewithmsmaxpro.me")
    
    // 3. Create the intent
    val intent = Intent(action, data)
    
    // 4. Start the activity
    startActivity(intent)
}

Example 2: Sharing Text

val shareButton = findViewById<Button>(R.id.share_button)
shareButton.setOnClickListener {
    val intent = Intent(Intent.ACTION_SEND)
    intent.type = "text/plain"
    intent.putExtra(Intent.EXTRA_SUBJECT, "Check out this cool website")
    intent.putExtra(Intent.EXTRA_TEXT, "https://codewithmsmaxpro.me")
    
    // Create a chooser dialog so the user can pick (e.g., WhatsApp, Twitter)
    startActivity(Intent.createChooser(intent, "Share via"))
}

Intent Filters

How does the system know that *your* app can handle an implicit intent? You declare an <intent-filter> in your AndroidManifest.xml. This is how you "register" your app to handle certain actions.

For example, to make your app appear in the "Share" dialog for text:

<activity android:name=".ShareReceiverActivity">
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="text/plain" />
    </intent-filter>
</activity>
Read More about Intents and Intent Filters →

Deep Dive: Services & Background Work

A **Service** is an application component that can perform long-running operations in the **background** without a user interface. It's used for tasks that should continue running even when the user leaves your app's main screen.

Warning: Services are one of the most misunderstood components. Since Android 8.0 (Oreo), the OS *heavily restricts* background services to save battery. You can no longer just run a service in the background for hours.

Types of Services:

  1. Started Service: "Fire and forget." You start it, it does its job, and it stops itself. Used for a single operation (e.g., uploading a user's photo).
  2. Bound Service: A client-server interface. The Activity "binds" to the service to communicate with it (e.g., a music player where the Activity needs to tell the service to "play," "pause," or "skip").
  3. Foreground Service: The *only* type of service that can run in the background for a long time. It *must* display a persistent, non-dismissible notification to the user (e.g., a music player's notification, a "navigating" notification from Google Maps, a "steps counted" notification).

Foreground Services: The Modern "Service"

If you need to play music or track a user's run, you must use a Foreground Service.

Step 1: Request Permission in Manifest (for Android 9+)

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

Step 2: Create the Service Class

class MyMusicService : Service() {
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // This method is called when startService() is used
        
        // 1. Create the Notification (MUST be done in 5 seconds)
        val notification = createNotification() // (See helper function below)

        // 2. Start the service in the foreground
        startForeground(1, notification) // 1 is a unique ID for the notification
        
        // 3. Start your long-running task (e.g., play music)
        playMusic()
        
        return START_STICKY // If killed by system, try to restart
    }
    
    private fun playMusic() { /* ... */ }
    private fun createNotification(): Notification { /* ... */ }

    override fun onBind(intent: Intent?): IBinder? {
        return null // We are not using binding
    }
}

Step 3: Start the Service from your Activity

val intent = Intent(this, MyMusicService::class.java)
// Use startForegroundService for modern Android
ContextCompat.startForegroundService(this, intent)

The *Real* Modern Way: `WorkManager`

The rules for background work are complex. What if you want to upload a file, but *only* when the phone is charging and on Wi-Fi? And what if the user closes the app?

For these "deferrable" (can run later) and "guaranteed" (will run eventually) tasks, Google created the **WorkManager** library. This is now the recommended solution for *most* background tasks that are not immediate (like playing music).

Step 1: Add the Dependency

implementation("androidx.work:work-runtime-ktx:2.9.0")

Step 2: Create a `Worker`

A `Worker` defines the *work* you want to do. It runs on a background thread automatically.

class UploadLogWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    
    override fun doWork(): Result {
        try {
            // Your background logic here
            uploadLogsToServer()
            return Result.success()
        } catch (e: Exception) {
            return Result.failure()
        }
    }
}

Step 3: Schedule the Work from your Activity

You create a `WorkRequest` that defines *when* this work should run.

// 1. Define the rules (Constraints)
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.UNMETERED) // Only on Wi-Fi
    .setRequiresCharging(true) // Only when charging
    .build()

// 2. Create the request
val uploadRequest = OneTimeWorkRequestBuilder<UploadLogWorker>()
    .setConstraints(constraints)
    .build()

// 3. Give it to the WorkManager to run
WorkManager.getInstance(this).enqueue(uploadRequest)

That's it! The OS will now guarantee that your UploadLogWorker will run whenever those conditions (Wi-Fi and charging) are met, even if the user has rebooted their phone.

Read More about Services & Background Work →

Deep Dive: Broadcast Receivers

A **Broadcast Receiver** is a component that does *one thing*: it listens for system-wide messages (broadcasts) and reacts to them. It has no UI. It's designed to be a "listener" or "trigger."

The system broadcasts messages for many events:

  • android.intent.action.BOOT_COMPLETED (Phone finished booting)
  • android.intent.action.ACTION_POWER_CONNECTED (Charger plugged in)
  • android.intent.action.AIRPLANE_MODE_CHANGED (Airplane mode toggled)

You can also send your own custom broadcasts from one part of your app to another.

Type 1: Static (Manifest-declared) Receivers

These are receivers you register in your AndroidManifest.xml. Their main power is that they can receive broadcasts **even if your app is not running**.

Problem: This was *heavily* restricted in Android 8 (Oreo) to save battery. You can no longer register for *most* implicit broadcasts (like network changes) in the manifest. Only a few, rare broadcasts (like ACTION_BOOT_COMPLETED) are still allowed.

Example: Run code on Boot

Step 1: Add Permission to Manifest

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

Step 2: Create the Receiver Class

class MyBootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
            Log.d("BootReceiver", "Phone just finished booting!")
            // Start a WorkManager job here, NOT a service
        }
    }
}

Step 3: Register in Manifest

<application ...>
    <receiver 
        android:name=".MyBootReceiver" 
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED" />
        </intent-filter>
    </receiver>
</application>

Type 2: Dynamic (Context-registered) Receivers

This is the modern, recommended way. You register a receiver *dynamically* from your Kotlin code (e.g., in your Activity). This receiver **only works while that component is running** (e.g., between onStart() and onStop()).

This is perfect for listening to events that only matter when your app is open (like "network connection lost").

Example: Listen for Charger Connection

class MainActivity : AppCompatActivity() {
    
    private val powerReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            if (intent?.action == Intent.ACTION_POWER_CONNECTED) {
                Log.d("PowerReceiver", "Charger connected!")
            }
        }
    }
    
    override fun onStart() { // Register when visible
        super.onStart()
        val filter = IntentFilter(Intent.ACTION_POWER_CONNECTED)
        registerReceiver(powerReceiver, filter)
    }

    override fun onStop() { // Unregister when invisible (VERY important)
        super.onStop()
        unregisterReceiver(powerReceiver)
    }
}
Read More about Broadcast Receivers →

Deep Dive: Content Providers

A **Content Provider** is a component that manages a shared set of app data. Its job is to provide a secure, database-like interface to *other apps* that want to access *your* app's data.

You will almost never need to *create* your own Content Provider. This is a very advanced topic, only needed if you are building an app (like a Contacts or Photos app) that needs to share its data with the world.

However, you will **frequently need to *use* (consume) Content Providers** built into the Android system.

Consuming a Content Provider (e.g., Reading Contacts)

The best example is the Android Contacts app. It stores all contacts in a database and exposes them to other apps (like WhatsApp, Telegram, or your app) via a Content Provider.

You don't access its database directly. You use a "resolver" to "query" a special "URL".

Key Concepts:

  • Content URI: A special URL that identifies the data. It's not http://, it's content://. For example, the URI for all contacts is ContactsContract.CommonDataKinds.Phone.CONTENT_URI.
  • ContentResolver: An object you get from your Context that lets you talk to the provider. You call contentResolver.query(...).
  • Cursor: An object that holds the results of your query (like a pointer to a database row). You loop through the cursor to read the data.

Example: Reading Contact Names

Step 1: Add Permission to Manifest

<uses-permission android:name="android.permission.READ_CONTACTS" />

Step 2: Query the Provider (in Kotlin)

// (You must also request this permission at runtime)
fun readContacts() {
    val projection = arrayOf(
        ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
        ContactsContract.CommonDataKinds.Phone.NUMBER
    )

    val cursor = contentResolver.query(
        ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
        projection,
        null, // No 'where' clause
        null, // No selection args
        null // Default sort order
    )

    cursor?.use { // 'use' automatically closes the cursor
        val nameIndex = it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
        
        while (it.moveToNext()) {
            val name = it.getString(nameIndex)
            Log.d("Contacts", "Name: $name")
        }
    }
}

This is a complex topic, but it shows how Android's components work together securely. You didn't access the Contacts app's database; you *asked* its Content Provider for data, and it gave you a `Cursor` with the results.

Read More about App Components & Fundamentals →