applicationId vs. namespace, version catalogR class
minSdk)
android14-6.1, android15-6.6, android16-6.12,
android17-6.18), vendor code lives in loadable modules with a stable interface (KMI)
| Dalvik VM (Android 1.0 - 4.4) | Android Runtime, ART (default since 5.0) |
|---|---|
| Register based VM (not stack based like the JVM) | Same, executes the same DEX bytecode |
| Interpreter, trace based JIT since 2.2 | Ahead-of-time (AOT) compilation to native code at install time (dex2oat) |
Install-time optimisation of the DEX file (dexopt) |
Since 7.0: JIT compiler + profile-guided compilation: hot methods are recorded while the app runs and precompiled when the device is idle and charging |
| Mark-and-sweep GC with long pauses | Concurrent, generational GC with short pauses |
| No debugging of optimized code | Better debugging and profiling support (sampling profiler, heap dumps) |
.dex files (Dalvik Executable): all classes of an app in one file, shared constant pool,
smaller than the sum of the .class files and mapped read-only into every process.class files
android-36), the compileSdk of your projectadb (Android Debug Bridge), fastbootaapt2 (compiles and packages resources, generates the R class), D8 (.class → .dex),
R8 (shrinks, obfuscates and optimizes release builds), apksigner, zipalign./gradlew assembleDebug, ./gradlew connectedAndroidTestplatform-tools of your SDK and add it to your PATH:~/Library/Android/sdk/platform-tools~/Android/Sdk/platform-tools%LOCALAPPDATA%\Android\Sdk\platform-toolsadb devices # list connected devices and running emulators
adb shell # open a shell on the device (-e: emulator, -d: USB device)
adb install app/build/outputs/apk/debug/app-debug.apk # install an APK
adb uninstall de.hdmstuttgart.movietracker # remove the app including its data (fresh start, used for grading)
adb push <local> <remote> # copy a file or directory to the device
adb pull <remote> [<local>] # copy a file or directory from the device
adb logcat # stream the device log (same as the Logcat window)
adb logcat -s MovieTracker # only lines with the tag MovieTracker
Log.d("MovieTracker", "clicked $counter times")applicationId vs. namespace, version catalogR classMovieTracker/
├── settings.gradle.kts modules of the project (here only :app) and repositories
├── build.gradle.kts root build file: declares the plugins (apply false)
├── gradle/libs.versions.toml version catalog: all library and plugin versions
├── local.properties SDK path (and secrets), NOT committed
└── app/
├── build.gradle.kts module build file: SDK versions, dependencies
└── src/
├── main/
│ ├── AndroidManifest.xml app components, permissions
│ ├── java/de/hdmstuttgart/movietracker/ Kotlin sources (yes, the folder is called java)
│ └── res/ resources: values/strings.xml, drawable/, mipmap-*/, xml/
├── test/ local JVM unit tests
└── androidTest/ instrumented tests running on a device (Assignment1Test.kt)
build/ folders and .gradle/ are generated, add them to .gitignoreapp/src/main/AndroidManifest.xml, one per module, read by the system when the app is installedIntents that these components can handle (intent filters)<uses-permission>)Application class, the launcher icon, the app label and the themeapplicationId, namespace, minSdk and
targetSdk are set in build.gradle.kts and merged into the manifest at build time<?xml version="1.0" encoding="utf-8"?>
<manifest>
<uses-permission/>
<application>
<activity>
<intent-filter>
<action/>
<category/>
<data/>
</intent-filter>
<meta-data/>
</activity>
<service>
<intent-filter>...</intent-filter>
<meta-data/>
</service>
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Needed to call the OMDb API and to load the poster images -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MovieTrackerApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MovieTracker"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.MovieTracker">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
<uses-permission android:name="android.permission.INTERNET">: without it every network call fails
(assignment 5). Dangerous permissions (camera, location) additionally need a runtime request (lecture 6)<application android:name=".MovieTrackerApplication">: your own Application subclass, created before any
other component. MovieTracker uses it to hold the dependencies (assignment 4). The leading . is short for
the namespaceandroid:icon, android:label, android:theme: resource references @mipmap/..., @string/..., @style/...<activity android:name=".MainActivity">: every Activity must be declared here, otherwise
startActivity() crashes with an ActivityNotFoundException (assignment 1: ResultActivity)android:exported="true": the Activity may be started by other apps (the launcher). Required explicitly for
every component with an intent filter since Android 12<intent-filter> with action.MAIN + category.LAUNCHER: this Activity gets the icon in the app drawertools:targetApi="31": only silences a lint warning, the tools namespace is stripped from the buildapp/build.gradle.ktsbuild.gradle)org.jetbrains.kotlin.android plugin is applied any more// app/build.gradle.kts of the MovieTracker reference solution
plugins {
alias(libs.plugins.android.application) // AGP 9.1: builds an app, compiles Kotlin
alias(libs.plugins.kotlin.compose) // Compose compiler plugin, version follows Kotlin
alias(libs.plugins.ksp) // Kotlin Symbol Processing, needed by Room (lecture 11)
}
// build.gradle.kts in the project root: declare the plugins once, modules apply them
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.ksp) apply false
}
libs.plugins.xyz are aliases from the version catalog (next slides)build.gradle.ktsandroid {
namespace = "de.hdmstuttgart.movietracker" // package of the generated R and BuildConfig classes
compileSdk = 36 // API you compile against: which classes/methods you can see
defaultConfig {
applicationId = "de.hdmstuttgart.movietracker" // unique id of the app on the device and in the store
minSdk = 26 // oldest Android version the app can be installed on
targetSdk = 36 // newest version whose behaviour changes you have tested
versionCode = 1 // integer, must grow with every release
versionName = "1.0" // string shown to users
}
}
compileSdk: always the newest stable API, otherwise new APIs and library versions are not availabletargetSdk: tells the system "I know about the rules up to this version". Google Play requires a recent
targetSdk for new apps and updates. Raising it can change behaviour (permissions, edge-to-edge, background limits)minSdk: trade-off between reach and effort. API 26 still covers almost every device in use; calling an API that
is newer than minSdk needs an if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.X) check (lint warns you)compileSdkapplicationId vs. namespaceapplicationId (Gradle defaultConfig): the identity of the appadb uninstall de.hdmstuttgart.movietracker and as the name of the app's private data folderapplicationIdSuffix = ".debug" lets debug and release build coexistnamespace (Gradle android block): the Kotlin package of the generated code (R, BuildConfig).MainActivity → de.hdmstuttgart.movietracker.MainActivity)package attribute of <manifest> was removed with AGP 8, the namespace lives in Gradle nowde.hdmstuttgart.movietracker for bothgradle/libs.versions.toml[versions]
agp = "9.1.0"
kotlin = "2.4.10"
composeBom = "2026.06.01"
navigationCompose = "2.9.8"
[libraries]
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
// app/build.gradle.kts: Gradle generates the type-safe accessor `libs`, dashes become dots
dependencies {
implementation(platform(libs.androidx.compose.bom)) // Bill of Materials: picks matching Compose versions
implementation(libs.androidx.material3) // no version: it comes from the BOM
implementation(libs.androidx.navigation.compose) // alias androidx-navigation-compose
}
libs.androidx.room.*, libs.retrofit and libs.coil.compose the same wayfindViewById, previews directly in Android Studio (@Preview)Button, TextField, Scaffold, TopAppBar, ...) are includedAndroidView, ComposeView) for existing appslibs.androidx.compose.bom) and the Compose compiler plugin<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"/>
</LinearLayout>
@Composable
fun HelloWorld() {
Column(modifier = Modifier.fillMaxSize()) {
Text(text = "Hello World!")
Button(onClick = { /*TODO*/ }) {
Text(text = "Button")
}
}
}
HelloWorld, MovieItem), lint warns otherwiseUnit and emit UI instead of returning a viewA composable function always has the annotation @Composable. Within the function the basic building blocks are
declared and describe the UI. It might contain several UI elements.
@Composable
fun MovieItem(movie: Movie, onMovieClick: (Movie) -> Unit) {
Image(
painter = painterResource(R.drawable.ic_movie_placeholder),
contentDescription = null
)
Text(text = movie.title)
Text(text = movie.year)
}
Without a layout the three elements are drawn on top of each other. In most cases you can just use the standard
layout elements Box, Row or Column to place the items on the screen:
@Composable
fun MovieItem(movie: Movie, onMovieClick: (Movie) -> Unit) {
Row {
Image(
painter = painterResource(R.drawable.ic_movie_placeholder),
contentDescription = null
)
Text(text = movie.title)
Text(text = movie.year)
}
}
Note: the screenshots show the real poster loaded from the web with the Coil library, which is introduced in lecture
12. Until then we use a drawable from res/drawable as placeholder.
There are also parameters to align the elements accordingly:
@Composable
fun MovieItem(movie: Movie, onMovieClick: (Movie) -> Unit) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = painterResource(R.drawable.ic_movie_placeholder),
contentDescription = null
)
Text(text = movie.title)
Text(text = movie.year)
}
}
You can also nest layouts like you need them:
@Composable
fun MovieItem(movie: Movie, onMovieClick: (Movie) -> Unit) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = painterResource(R.drawable.ic_movie_placeholder),
contentDescription = null
)
Column {
Text(text = movie.title)
Text(text = movie.year)
}
}
}
A special parameter is the Modifier. It changes size, layout and behaviour of an element and adds information such
as accessibility labels or test tags. It also makes elements clickable, which finally uses the onMovieClick parameter:
@Composable
fun MovieItem(movie: Movie, onMovieClick: (Movie) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.testTag("movieItem") // the UI test finds the item by this tag
.clickable { onMovieClick(movie) } // whole row reacts to taps
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.drawable.ic_movie_placeholder),
contentDescription = null,
modifier = Modifier.width(80.dp).padding(8.dp)
)
Column {
Text(text = movie.title, fontSize = 20.sp)
Text(text = movie.year)
}
}
}
.clickable { }.padding(8.dp) makes the padding part of the touch area, .padding(8.dp).clickable { } does notfillMaxWidth(), width(), height(), padding(), background(), clickable(), testTag(),
weight() (inside Row/Column) are the ones you will use mostmodifier: Modifier = Modifier as first optional parameter and passes it to its
root element, so the caller can position it. See Compose modifiersandroidx.compose.material3Scaffold which provides the structure of a screen (app bar, content, floating action button) and the
innerPadding you must apply to the content so it does not hide behind system barsTopAppBar to display a title and provide navigationText to display textButton to make clickable elements, with a composable content lambda for its labelTextField / OutlinedTextField for text inputLazyColumn / LazyRow for scrollable lists (lecture 7, assignment 2)HorizontalDivider, Card, Checkbox, Switch, Slider, LinearProgressIndicator, ...MaterialTheme provides colors, typography and shapes: MaterialTheme.typography.titleMedium,
MaterialTheme.colorScheme.error. The theme of your project lives in ui/theme/Theme.ktsetContentThe MainActivity is the entry point of the UI. onCreate() belongs to the Activity lifecycle (lecture 6) and is
called by the system. In there you call the first composable inside setContent { }:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge() // draw behind the system bars (default since Android 15)
setContent { // the Compose tree replaces setContentView(R.layout.xyz)
MovieTrackerTheme { // Material theme generated by the project template
MovieTrackerApp() // your first composable
}
}
}
}
ComponentActivity comes from androidx.activity, setContent from androidx.activity.composesetContent is Compose, the Activity itself stays a thin hostMAIN/LAUNCHER intent filter decides which Activity starts firstremember and mutableStateOfvar counter = 0 inside it is reset every time the function runs againremember) and one that Compose can observe
(mutableStateOf)@Composable
fun ClickCounter() {
var counter by remember { mutableIntStateOf(0) } // State<Int>: observed, kept across recompositions
Column {
Text(text = "Clicked $counter times") // reads the state
Button(onClick = { counter++ }, modifier = Modifier.testTag("button")) { // writes the state
Text(text = "Button")
}
}
}
val counterState = remember { mutableStateOf(0) } and use counterState.valuevar counter by remember { mutableStateOf(0) } (needs import androidx.compose.runtime.getValue/setValue)val (counter, setCounter) = remember { mutableStateOf(0) } (destructuring)mutableIntStateOf, mutableLongStateOf, ... avoid boxing for primitives; mutableStateOf works for every typeMutableState that was read during composition is written (counter++)Text), not the whole screenLog spam, no object creation that must happen once)remember { }list.add(...)) is invisible to Compose, only state objects trigger recompositionremember stores the value in the composition, keyed by its position in the UI tree. Leaving the screen
(or removing the item from a LazyColumn) forgets itdata classes
are a good fit. See Thinking in ComposerememberSaveable: Surviving Rotationremember lives as long as the composable is on screen. When the Activity is recreated (device rotation,
dark mode switch, language change, process killed in the background) the whole composition is rebuilt and
the state is lost: the counter jumps back to 0rememberSaveable additionally stores the value in the saved instance state Bundle and restores itInt, String, Boolean, ..., Parcelable and Serializable; other types need a Saver// assignment 1 (results/assignment1/MainActivity.kt)
var counter by rememberSaveable { mutableIntStateOf(0) }
// SearchScreen.kt: the text in the search field survives a rotation
var searchText by rememberSaveable { mutableStateOf("") }
rememberSaveable;
transient things (animation progress, scroll offset of a list that remembers itself) → rememberViewModel (lecture 11)remember. Stateless composable: gets the value and a callback as
parameters. Hoisting = moving the state up to the lowest common ancestor that needs it@Composable
fun ClickCounter() { // stateful: owns the state
var counter by rememberSaveable { mutableIntStateOf(0) }
CounterContent(counter = counter, onIncrement = { counter++ })
}
@Composable
fun CounterContent(counter: Int, onIncrement: () -> Unit) { // stateless: reusable, testable, previewable
Column {
Text(text = "Clicked $counter times")
Button(onClick = onIncrement, modifier = Modifier.testTag("button")) { Text(text = "Button") }
}
}
TextField does not store its text, the caller does:@Composable
fun HelloContent() {
var name by rememberSaveable { mutableStateOf("") } // owner of the state
OutlinedTextField(value = name, onValueChange = { name = it }, label = { Text("Name") }) // value down, event up
if (name.isNotEmpty()) Text(text = "Hello, $name!")
}
SearchScreen.kt does the same: TextField(value = searchText, onValueChange = { searchText = it })
Later the ViewModel becomes the owner (viewModel.searchMovies(searchText)), the composables stay the same
mutableStateListOf// results/assignment2/MainActivity.kt
@Composable
fun MovieList(modifier: Modifier = Modifier) {
val movieList = remember {
mutableStateListOf( // SnapshotStateList<Movie>: observed by Compose
Movie("Dr. No", "1962", "Sean Connery"),
Movie("From Russia with Love", "1963", "Sean Connery")
)
}
LazyColumn(modifier = modifier) {
items(items = movieList) { movie ->
MovieItem(movie = movie, onMovieClick = { movieList.remove(it) }) // triggers recomposition
}
}
}
remember { mutableListOf(...) }: the list survives recomposition, but remove() is not observed, the
LazyColumn never updates. A classic bug in assignment 2remember { mutableStateOf(listOf(...)) }: works, but only when you assign a new list
(movies = movies - movie), mutating the old one changes nothingmutableStateListOf() / mutableStateMapOf(): observed list and map, add/remove recomposeFlow and is collected as state (lectures 10 and 11)app/src/main/res/, grouped by type:res/values/strings.xml, colors and themes in res/values/colors.xml, themes.xmlres/drawable/ (ic_movie_placeholder.xml, PNG/WebP/XML vector drawables)res/mipmap-*/ (the system scales them for the home screen; do not put images here)res/xml/ (backup_rules.xml, data_extraction_rules.xml)res/layout/ for View-based XML layouts, not needed with Jetpack Compose@string/app_name, @drawable/ic_movie_placeholder
and from Kotlin via the R class:Image(painter = painterResource(R.drawable.ic_launcher_foreground), contentDescription = null)
Text(text = stringResource(R.string.app_name))
Text(text = stringResource(R.string.search_error, message)) // "Could not load movies: %1$s"
<!-- res/values/strings.xml: never hard-code user visible text, strings.xml allows translations -->
<resources>
<string name="app_name">MovieTracker</string>
<string name="search_error">Could not load movies: %1$s</string>
</resources>
Create a new directory in res/ named in the form <resources_name>-<config_qualifier>
<resources_name> is the directory name of the corresponding default resources<qualifier> is a name that specifies an individual configuration for which these resources
are to be usedSave the respective alternative resources in this new directory. The resource files must be named exactly the same as the default resource files
res/
drawable/
background.png
drawable-hdpi/
background.png
values/
strings.xml
values-de/
strings.xml
values-night/
colors.xml
hdpi qualifier indicates that the resources in that directory are for devices with a
high-density screen, de selects the German strings, night the dark mode colors.xml) scale without density folders, which is why icons are vectors today
Sizes of UI components should be defined in Density Independent Pixels (dp) in order to always get the best results.
More information here.
dp) describes the size of a UI element in a density independent
manner160ppi, one dp corresponds to one physical pixel (px)160ppi, the dp values are converted with the help of the
densityFactordensityFactor describes the device's physical density in relation to the default density of
160ppisp (scale-independent pixels): like dp, but additionally scaled by the user's font size settingModifier.width(80.dp) and fontSize = 20.sp, the conversion to px is done for youdensityFactor at runtime:val densityFactor = LocalDensity.current.density
R: one nested class per resource type
(R.string, R.drawable, R.mipmap, ...) with an Int constant per resourceaapt2 on every build into the namespace package (de.hdmstuttgart.movietracker.R).
There is no R.java file in your source tree, it lives in app/build/ and you never edit itR class is non-transitive: it only contains the resources of its own module.
Resources of a library are referenced via the library's R class (androidx.compose.material3.R)Int that the resource framework resolves at runtime for the current configurationval titleId: Int = R.string.app_name // e.g. 0x7f0f0000
Text(text = stringResource(R.string.app_name)) // Compose: picks the right strings.xml
val label = LocalContext.current.getString(R.string.app_name) // classic framework call
R in red: the build has not run yet or a resource file contains an error (check the Build tab)androidx.*, versions in the version catalogandroid.support.*, com.android.support:appcompat-v7), which you still meet in old code and Stack Overflow answersComponentActivity vs. AppCompatActivityandroidx.activity.ComponentActivityViewModelStore, saved state, OnBackPressedDispatcher and the Activity Result APIsetContent { } is an extension function from androidx.activity:activity-composeandroidx.appcompat.app.AppCompatActivity (extends FragmentActivity, which extends ComponentActivity)ActionBar, Fragment support and backports of View widgetsComponentActivity, keep the Activity thin and put the UI into composablesres/layout/, inflated into a tree of View objects
(setContentView(R.layout.activity_main)), accessed with findViewById() or View BindingViewGroups) you will meet in existing apps and in old tutorials:LinearLayout: children in one row or column, layout_weight distributes spaceRelativeLayout: children positioned relative to each other or the parentFrameLayout: children stacked on top of each otherConstraintLayout (AndroidX): flat hierarchy, children positioned by constraints, built with the Layout EditorRecyclerView: the list component, replaced by LazyColumn (lecture 7)Column/Row (LinearLayout), Box (FrameLayout), ConstraintLayout also exists
for ComposeAndroidView { } embeds a View in Compose, ComposeView embeds Compose in XML
(see Interoperability APIs)
.class files.class files (and those of all libraries) into .dex files; in release builds R8 additionally
shrinks, obfuscates and optimizes the code (isMinifyEnabled = true)aapt2 compiles the resources, generates the R class and the binary AndroidManifest.xml.aab for the Play Store)adb install copies it to the device, ART compiles it on installation and while it runs
./gradlew assembleDebug builds app/build/outputs/apk/debug/app-debug.apk,
./gradlew installDebug also installs it, ./gradlew connectedAndroidTest runs the UI tests on a connected devicesettings.gradle.kts (modules, repositories), root build.gradle.kts (plugins),
app/build.gradle.kts (SDK versions, build types, dependencies), gradle/libs.versions.toml (versions)gradle/wrapper/gradle-wrapper.properties (Gradle 9.3 for AGP 9.1),
so everybody builds with the same Gradle without installing itAssignment 1: create the MovieTracker project, a counter with Text and Button, and open a
second Activity after 5 clicks.
What you need from this lecture:
de.hdmstuttgart.movietracker, Kotlin DSL, minSdk 26, an AVD in the Device ManagerColumn, Text and Button inside setContent { MovieTrackerTheme { ... } }var counter by rememberSaveable { mutableIntStateOf(0) } and counter++ in onClick; the Text
recomposes by itselfModifier.testTag("button") on the Button, the UI test finds it by this tagAndroidManifest.xml: <activity android:name=".ResultActivity" />Intent and reading the extra is explained in lecture 6
(Android Framework Basics); the assignment text contains the snippet you needAssignment1Test.kt in app/src/androidTest/, right-click → Run, or ./gradlew connectedAndroidTestcompileSdk/targetSdk/minSdk), applicationId and namespace live in build.gradle.kts,
library versions in gradle/libs.versions.toml@Composable functions in PascalCase, layouts with
Column/Row/Box, Modifiers in a meaningful order, Material 3 componentsremember { mutableStateOf() } (or rememberSaveable to survive rotation), writes trigger
recomposition; state is hoisted and flows down while events flow up; lists use mutableStateListOfres/ (strings in values/strings.xml, images in drawable/, launcher icons in mipmap-*/)
and are accessed through the generated, non-transitive R class; qualifiers select alternatives per configurationComponentActivity.class files?compileSdk, targetSdk and minSdk? Which one decides whether a user can install
the app?applicationId vs. namespace: which one must never change after the first release and why?android:exported="true" mean?var counter = 0 inside a composable not work as a counter? What do remember and mutableStateOf add,
and when do you need rememberSaveable?TextField(value = ..., onValueChange = ...).remember { mutableListOf(...) } not update a LazyColumn when an item is removed, and what do you use instead?res/ directories do strings, images and launcher icons belong? What are resource qualifiers used for?R class on Android? By whom and when is it created?libs.androidx.navigation.compose in build.gradle.kts refer to?