Open menu with table of contents Android Framework Basics
Logo of Stuttgart Media University for light theme Logo of Stuttgart Media University for dark theme
Android Development

Android Framework Basics

Stuttgart Media University

1 Agenda

  • Android App Components
  • Activities
    • Activity Lifecycle
    • Activity as Compose host
    • Lifecycle in Compose
  • Application Context
  • Intents
    • Explicit and implicit Intents
    • Intent Filters
  • Assignment 1
  • Android Security & Permissions
  • WebView & ProgressBar

2 Android App Components

The most important building blocks of an Android app:

  • Activity: one screen of your app. With Jetpack Compose the Activity is a thin host: it calls setContent { } and owns the lifecycle, the UI itself lives in composables (and, from lecture 11 on, the UI state lives in a ViewModel)
  • Fragment (legacy): modularisation of a screen in the View system. Compose does not need Fragments, we use Navigation Compose instead (lecture 08)
  • Service: runs code without a UI, e.g. a long-running download (lecture 09)
  • BroadcastReceiver: receives messages (events) from the system, other apps or your own app (lecture 09)
  • ContentProvider: exposes data of your app to other apps, e.g. the contacts provider
  • Intent: message object for inter-component communication, used e.g. to start an Activity and to transfer information to it

All components except Intent must be declared in AndroidManifest.xml.

3 Activities

4 Activity

  • An app can have multiple Activities, each one defines one visible screen
  • The system creates, pauses, stops and destroys an Activity: the Activity defines the lifecycle of your UI
  • In the View system the Activity also contained the UI logic; with Compose it only hosts the UI
    • ComponentActivity is the base class for Compose apps (AppCompatActivity is for XML layouts)
    • setContent { } defines the UI as a tree of composables
  • Most apps written with Compose have exactly one Activity and switch between screens with Navigation Compose (lecture 08). Assignment 1 uses two Activities to show how Intents work

4.1 Activity Lifecycle

center

4.2 Activity Lifecycle

An activity has essentially four states:

  • If an activity is in the foreground of the screen (at the top of the stack), it is active or running.
  • If an activity has lost focus but is still visible (that is, a new non-full-sized or transparent activity has focus on top of your activity), it is paused. A paused activity is completely alive (it maintains all state and member information and remains attached to the window manager), but can be killed by the system in extreme low memory situations.
  • If an activity is completely obscured by another activity, it is stopped. It still retains all state and member information, however, it is no longer visible to the user so its window is hidden and it will often be killed by the system when memory is needed elsewhere.
  • If an activity is paused or stopped, the system can drop the activity from memory by either asking it to finish, or simply killing its process (it is finished or killed). When it is displayed again to the user, it must be completely restarted and restored to its previous state.

4.3 Activity Lifecycle Callbacks

  • The Activity class extends Context and provides the lifecycle callbacks
  • To react on lifecycle changes, override them and always call super
class MainActivity : ComponentActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)   // called once, set up the UI here
  }

  override fun onStart() { super.onStart() }       // becoming visible

  override fun onResume() { super.onResume() }     // in the foreground, interactive

  override fun onPause() { super.onPause() }       // losing focus, still visible

  override fun onStop() { super.onStop() }         // no longer visible

  override fun onRestart() { super.onRestart() }   // stopped -> started again

  override fun onDestroy() { super.onDestroy() }   // finishing or being recreated
}
  • Rotating the device destroys and recreates the Activity: everything that is not saved is lost

4.4 Activity as Compose Host

This is the complete MainActivity of assignment 1:

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MovieTrackerTheme {
                ClickCounter()
            }
        }
    }
}
  • setContent (from androidx.activity:activity-compose) attaches the Compose UI to the Activity
  • Nothing else happens in the Activity: UI and state are in the ClickCounter composable
  • State that should survive the recreation on rotation is kept with rememberSaveable (lecture 05)

4.5 Lifecycle in Compose

Composables do not override lifecycle callbacks. They use effects that follow the composition and the lifecycle of the host:

@Composable
fun MovieScreen(onRefresh: () -> Unit) {
    // Runs once when MovieScreen enters the composition (coroutine, may call suspend functions)
    LaunchedEffect(Unit) { onRefresh() }

    // Runs when entering, onDispose runs when MovieScreen leaves the composition
    DisposableEffect(Unit) {
        Log.d("MovieScreen", "shown")
        onDispose { Log.d("MovieScreen", "hidden") }
    }

    // Follows the Activity lifecycle: ON_RESUME / ON_PAUSE (androidx.lifecycle:lifecycle-runtime-compose)
    LifecycleResumeEffect(Unit) {
        Log.d("MovieScreen", "resumed")
        onPauseOrDispose { Log.d("MovieScreen", "paused") }
    }
}
  • LifecycleEventEffect(Lifecycle.Event.ON_START) { ... } and LifecycleStartEffect exist as well
  • Coroutines and LaunchedEffect are covered in detail in lecture 10

5 Application Context

  • The Context class is the interface to global information about the application environment
  • It is an abstract class whose implementation is provided by the Android system; Activity, Service and Application are Contexts
  • It gives access to resources, system services and to "up-calls" into the framework, for example:
// launches a new activity
abstract fun startActivity(intent: Intent)

// launches a new service
abstract fun startService(service: Intent): ComponentName?

// registers a BroadcastReceiver
abstract fun registerReceiver(receiver: BroadcastReceiver?, filter: IntentFilter): Intent?
  • Inside a composable you get the current Context with val context = LocalContext.current

6 Intents

7 Intent

  • An Intent is an abstract description of an operation to be performed
  • It can be used
    • to start an activity (implicitly or explicitly)
    • as broadcast to inform other applications of events
    • to start, stop and communicate with services
    • to access data from content providers

7.1 Two Types of Intents

  • Explicit intents specify the component to start by name (the fully-qualified class name). Typical use: start another Activity or a Service of your own app
  • Implicit intents do not name a specific component, but instead declare a general action to perform, which allows a component from another app to handle it. Typical use: open a web page, dial a number, share a text

7.2 Explicit Intent

  • Explicitly name the class of the component to be called
  • To transfer additional information from one activity to another, use putExtra()
    • Extras are key-value pairs of simple types such as Int, String, Boolean, ...
  • Example from assignment 1 (MainActivity.kt): start ResultActivity after the 5th click
@Composable
fun ClickCounter() {
    var counter by rememberSaveable { mutableIntStateOf(0) }
    val context = LocalContext.current   // the Context is needed to start another Activity

    Button(
        onClick = {
            counter++
            if (counter == 5) {
                val intent = Intent(context, ResultActivity::class.java)
                intent.putExtra(ResultActivity.EXTRA_CLICKS, counter)
                context.startActivity(intent)
            }
        },
        modifier = Modifier.testTag("button")
    ) {
        Text(text = "Button")
    }
}

7.3 Explicit Intent: Reading the Extras

  • The started Activity gets the Intent as property intent and reads the extras with the typed getters (getIntExtra, getStringExtra, ...) and a default value
class ResultActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Read the value that MainActivity put into the Intent
        val clicks = intent.getIntExtra(EXTRA_CLICKS, 0)

        setContent {
            MovieTrackerTheme {
                Text(text = "Clicks: $clicks")
            }
        }
    }

    companion object {
        const val EXTRA_CLICKS = "de.hdmstuttgart.movietracker.EXTRA_CLICKS"
    }
}
  • Use a constant for the key: the key is shared by sender and receiver, a typo is not detected by the compiler

Note: An explicit intent is always delivered to its target, regardless of any intent filters the component declares.

7.4 Explicit Intent: Manifest Entry

  • Every Activity must be declared in AndroidManifest.xml, otherwise startActivity() throws an ActivityNotFoundException
<application ...>
    <activity
        android:name=".ResultActivity"
        android:exported="false" />
    <activity
        android:name=".MainActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>
  • android:name=".ResultActivity" is relative to the namespace declared in build.gradle.kts
  • android:exported="false": only your own app can start this Activity (mandatory attribute since Android 12 for components with an intent filter)

7.5 Implicit Intent

  • An implicit intent specifies an action that can invoke any app on the device able to perform the action
  • The action might require some specific data (a Uri)
  • Useful when your app cannot perform the action, but other apps probably can, and you'd like the user to pick which app to use
  • Example: dial a number or open a web page (more examples here)
@Composable
fun ContactButtons(phoneNumber: String, website: String) {
    val context = LocalContext.current

    Row {
        Button(onClick = {
            val dialIntent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:$phoneNumber"))
            context.startActivity(dialIntent)
        }) { Text(text = "Call") }

        Button(onClick = {
            val viewIntent = Intent(Intent.ACTION_VIEW, Uri.parse(website))
            context.startActivity(viewIntent)
        }) { Text(text = "Website") }
    }
}

7.6 Implicit Intent

center

  1. Activity A creates an Intent with an action description and passes it to startActivity()
  2. The Android System searches all apps for an intent filter that matches the intent
  3. When a match is found, the system starts the matching activity (Activity B) by invoking its onCreate() method and passing it the Intent

7.7 Intent Filter

  • To advertise which implicit intents your app can receive, declare one or more intent filters for each of your app components with an <intent-filter> element in your manifest file
  • Each intent filter specifies the type of intents it accepts based on the intent's action, data, and category
  • The system delivers an implicit intent to your app component only if the intent can pass through one of your intent filters
  • Inside the <intent-filter>, you can specify the type of intents to accept using one or more of these three elements:
    • <action> - Declares the intent action accepted, in the name attribute. The value must be the literal string value of an action, not the class constant
    • <data> - Declares the type of data accepted, using one or more attributes that specify various aspects of the data URI (scheme, host, port, path) and MIME type
    • <category> - Declares the intent category accepted, in the name attribute

7.8 Intent Filter

  • Example Intent-Filter declaration: an Activity that can open https:// links
<activity android:name=".MainActivity" android:exported="true">
    <!-- This activity is the main entry, should appear in app launcher -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <!-- This activity can also handle ACTION_VIEW for https links -->
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="www.hdm-stuttgart.de" />
    </intent-filter>
</activity>

Note: To receive implicit intents, you must include the CATEGORY_DEFAULT category in the intent filter.

8 Assignment 1

Assignment 1: Introduction to Android Studio and your first app

What you need from this lecture for it:

  • ComponentActivity + setContent { } as the host of your Compose UI (two Activities)
  • An explicit Intent with putExtra() to start ResultActivity after the 5th click, and intent.getIntExtra() to read the value in the second Activity
  • LocalContext.current to get the Context for startActivity() inside a composable
  • The <activity> entry in AndroidManifest.xml for the second Activity

9 Android Security & Permissions

9.1 Android Security Architecture: Basic Principles

  • Install time verification
    • APK files are signed by the developer and verified at install time (and by Google Play Protect)
  • Application sandboxing (process separation)
    • Each application runs in its own process with its own unique Linux user ID
    • Only has (write) access to its own data
    • Other apps' resources are only available through defined interfaces (Intents, ContentProviders)
  • Permissions
    • Access to protected features and data is granted by the user
    • Normal permissions are granted automatically at install time (e.g. INTERNET)
    • Dangerous permissions are requested from the user at runtime since Android 6.0 (API 23); on older devices they were granted at install time

9.2 Android Permissions

  • Applications by default have no permission to perform operations that might impact other applications, the user's privacy or the device
  • Every permission an app needs must be declared in AndroidManifest.xml
  • Predefined permissions exist, e.g. INTERNET, CAMERA, ACCESS_FINE_LOCATION, ...
  • Permissions are enforced at runtime: using a protected API without the permission throws a SecurityException
  • The user can revoke a dangerous permission any time in the system settings, so check it every time before you use the protected feature

9.4 Android Permissions Declaration

  • To make use of protected features of the device, add <uses-permission> elements to the manifest
  • For example, an app that takes photos with the camera declares:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.CAMERA" />
    ...
</manifest>
  • The most common protection levels:
    • Normal permission - little risk for the user's privacy - granted automatically when the app is installed (e.g. INTERNET, FOREGROUND_SERVICE)
    • Dangerous permission - access to the user's private data or device features - the user is asked for approval in a system dialog at runtime (e.g. CAMERA, ACCESS_FINE_LOCATION, POST_NOTIFICATIONS)
  • Both types must be declared in the manifest, dangerous permissions must additionally be requested at runtime

9.5 Requesting Permissions at Runtime

To request a dangerous permission in Compose:

@Composable
fun CameraPermissionButton(onGranted: () -> Unit) {
    val context = LocalContext.current
    var message by remember { mutableStateOf("") }

    // 1. Register the launcher unconditionally at the top of the composable
    val permissionLauncher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted ->
        if (isGranted) onGranted() else message = "Camera permission denied"
    }

    Column {
        Button(onClick = {
            // 2. Check first, the user may have revoked the permission in the meantime
            val granted = ContextCompat.checkSelfPermission(
                context, Manifest.permission.CAMERA
            ) == PackageManager.PERMISSION_GRANTED
            // 3. Use the feature or ask the user (system dialog)
            if (granted) onGranted() else permissionLauncher.launch(Manifest.permission.CAMERA)
        }) {
            Text(text = "Take photo")
        }
        Text(text = message)
    }
}

9.6 Requesting Permissions at Runtime

  • rememberLauncherForActivityResult must be called on every composition (never inside an if or a click handler), the result callback replaces the old onRequestPermissionsResult()
  • RequestPermission() asks for one permission, RequestMultiplePermissions() for several at once
  • If the user denied the permission before, the system expects you to explain why your app needs it before asking again (step "show rationale" in the workflow diagram)
    • ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA) tells you if that is the case. It is an Activity method, in Compose get the Activity with LocalActivity.current (activity-compose)
  • After two denials the system dialog is no longer shown, the result is false immediately: offer a way into the app settings or disable the feature gracefully

10 WebView & ProgressBar

10.1 WebView

  • android.webkit.WebView renders web pages inside your app
  • The alternative is to let the browser do the work with an implicit Intent:
val context = LocalContext.current
...
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://www.hdm-stuttgart.de")))
  • Embedding a WebView is useful for content that belongs to your app (help pages, OAuth logins, hybrid apps)
  • Requires the INTERNET permission in AndroidManifest.xml (a normal permission, granted at install time):
<uses-permission android:name="android.permission.INTERNET" />

10.2 WebView in Compose

  • There is no Compose WebView. Views that have no Compose equivalent are embedded with AndroidView
  • Configure the View in the factory lambda, which runs once. The update lambda runs on every recomposition, so never call loadUrl() there
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun WebViewScreen(url: String, modifier: Modifier = Modifier) {
    AndroidView(
        modifier = modifier.fillMaxSize(),
        factory = { context ->
            WebView(context).apply {
                // Keep clicked links inside the WebView instead of opening the browser
                webViewClient = WebViewClient()
                // JavaScript is disabled by default for security reasons
                settings.javaScriptEnabled = true
                loadUrl(url)
            }
        }
    )
}
  • Without a WebViewClient, clicking a link launches the app that handles URLs (usually the browser)
  • Enabling JavaScript requires the @SuppressLint("SetJavaScriptEnabled") annotation (lint warning)

10.3 ProgressBar

  • The WebView loads pages asynchronously. Register a WebChromeClient to get the loading progress and put it into Compose state:
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun WebViewScreen(url: String, modifier: Modifier = Modifier) {
    var progress by remember { mutableFloatStateOf(0f) }

    Column(modifier = modifier) {
        if (progress < 1f) {
            LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth())
        }
        AndroidView(
            modifier = Modifier.fillMaxSize(),
            factory = { context ->
                WebView(context).apply {
                    webViewClient = WebViewClient()
                    settings.javaScriptEnabled = true
                    webChromeClient = object : WebChromeClient() {
                        override fun onProgressChanged(view: WebView?, newProgress: Int) {
                            progress = newProgress / 100f
                        }
                    }
                    loadUrl(url)
                }
            }
        )
    }
}

Note: LinearProgressIndicator and CircularProgressIndicator without a progress lambda show an indeterminate animation, useful whenever something loads in the background (e.g. a network call).

11 Summary

  • An Activity is one screen; with Compose it is a thin host that calls setContent { } and owns the lifecycle. The system recreates it on rotation
  • Composables react to the lifecycle with effects: LaunchedEffect, DisposableEffect, LifecycleResumeEffect
  • Explicit Intents start a component by class name and carry extras; the target reads them with intent.getIntExtra() etc. Every Activity must be declared in the manifest
  • Implicit Intents describe an action and let the system pick a matching app via intent filters
  • Apps run in a sandbox. Normal permissions are granted at install time, dangerous permissions are requested at runtime (since Android 6.0) with rememberLauncherForActivityResult and can be revoked by the user at any time
  • A WebView is embedded with AndroidView and configured in the factory lambda; a progress indicator shows background loading

12 Recap Questions

  • What lifecycle states does an Activity have? Can you name the lifecycle callbacks?
  • What happens to your UI state when the device is rotated? How do you preserve it?
  • Which Compose effect do you use to run cleanup code when a composable leaves the screen?
  • What types of intents exist? What is the difference?
  • How do you transfer information from one Activity to another, and how is it read on the other side?
  • Why must every Activity be declared in the manifest?
  • How does Android prevent apps from accessing the data of other apps?
  • What is the difference between a normal and a dangerous permission? Name an example for each
  • Why must rememberLauncherForActivityResult be called unconditionally in a composable?
  • Why is the WebView configured in the factory lambda of AndroidView and not in update?
  • How do you prevent a WebView from opening clicked links in the system browser?

Questions?