The first successful Run click is a lovely moment: a project that was just folders and unfamiliar Gradle files becomes something you can touch. I prefer a first app that does slightly more than print static text, so this one remembers a name while the screen is alive, updates immediately, and gives us enough behavior to preview, test, debug, and package.
What you will build
A single-activity Kotlin app using Jetpack Compose.
An outlined name field and a greeting derived from current UI state.
A preview that renders without launching an emulator.
A pure formatting function with a local unit test.
A debug build installed on an emulator or USB/Wi-Fi-connected device.
A reproducible Gradle wrapper build and a known APK output path.
Before creating the project
Install a current stable Android Studio from the official source and complete its Setup Wizard.
Install the Android SDK platform and tools selected by the project template.
Keep enough disk/RAM for Gradle dependencies and an emulator image; a physical device is a good alternative on constrained machines.
Use version control from the first working project and exclude generated/local files through the template
.gitignore.Choose an application ID/package namespace you control for a real product;
com.exampleis fine only for learning.Decide the oldest Android version your users need before choosing Minimum SDK; a lower minimum expands reach but increases compatibility work.
1. Create an Empty Activity project
Launch Android Studio and select New Project.
Under Phone and Tablet, choose Empty Activity—the Compose template, not an older Views template with a similar name.
Set Name to
First Compose App.Use a learning namespace such as
com.example.firstcomposeapp, or your organization’s reverse-domain namespace.Choose a local project directory that is backed up and does not synchronize generated files poorly.
Keep Kotlin as the language and select a Minimum SDK that matches your target audience and dependency requirements.
Finish and wait for indexing plus Gradle sync to complete before editing generated versions.
2. Read the generated project before changing it
settings.gradle.ktsnames the build and includes modules such as:app.The root and module
build.gradle.ktsfiles configure plugins, Android options, Compose, and dependencies.gradle/libs.versions.tomlmay centralize plugin and library versions in current templates.app/src/main/AndroidManifest.xmldeclares application components and capabilities.app/src/main/java/.../MainActivity.ktcontains the activity and starter composables.app/src/main/res/stores resources such as strings, icons, colors, and XML configuration.app/src/test/contains local JVM tests;app/src/androidTest/contains device/emulator tests.gradle/wrapper/gradle-wrapper.propertiespins the Gradle distribution used bygradlew.
3. Build a stateful Compose screen
package com.example.firstcomposeapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.firstcomposeapp.ui.theme.FirstComposeAppTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
FirstComposeAppTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
GreetingScreen(
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
fun greetingFor(name: String): String {
val trimmed = name.trim()
return if (trimmed.isEmpty()) "Hello, Android!" else "Hello, $trimmed!"
}
@Composable
fun GreetingScreen(modifier: Modifier = Modifier) {
var name by rememberSaveable { mutableStateOf("") }
Column(
modifier = modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = greetingFor(name),
style = MaterialTheme.typography.headlineMedium,
)
OutlinedTextField(
value = name,
onValueChange = { name = it },
modifier = Modifier.fillMaxWidth(),
label = { Text("Your name") },
singleLine = true,
)
}
}
@Preview(showBackground = true)
@Composable
private fun GreetingScreenPreview() {
FirstComposeAppTheme {
GreetingScreen()
}
}The UI is a function of state
ComponentActivityhosts Compose content instead of inflating an XML layout.setContentdefines the root composition; the generated Material theme supplies typography and colors.Scaffoldprovides content insets, and applyinginnerPaddingkeeps edge-to-edge UI clear of system bars.rememberSaveableretains the text across recomposition and supported state restoration such as rotation; it is not permanent database storage.Changing
nameschedules recomposition of code that reads it.OutlinedTextFieldreceives state and emits edits throughonValueChange, demonstrating unidirectional data flow.The pure
greetingForfunction has no Android dependency and is easy to unit test.@Previewrenders the composable in Android Studio without installing the app.
4. Move visible text into resources for a real app
Hard-coded English keeps the first code block readable, but production UI should use string resources so translation, accessibility review, and consistency are manageable. Compose reads them with stringResource(). Dynamic values belong in formatted resources rather than string concatenation when localization matters.
<resources>
<string name="app_name">First Compose App</string>
<string name="name_label">Your name</string>
<string name="greeting_default">Hello, Android!</string>
<string name="greeting_named">Hello, %1$s!</string>
</resources>Resources separate language from layout code
Each string has a stable resource name and a default-locale value.
%1$sis a positional placeholder that translators can reorder.User input passed to
stringResource(R.string.greeting_named, trimmed)remains data, not a resource identifier.Add locale-specific
values-xxdirectories only with reviewed translations.Keep the application label in resources so the manifest and UI can share it.
5. Render and interact with the preview
Open the Kotlin file and select Split or Design mode when the Compose preview pane is available.
Build/refresh the preview after Gradle sync and code compilation finish.
Use interactive preview to type into the field, while remembering it is not a full device runtime.
Add previews for light/dark themes, font scaling, long names, and different widths as the screen grows.
Treat preview compilation errors like normal Kotlin/build errors; read the first relevant diagnostic.
6. Create an emulator or connect a phone
In Device Manager, create an Android Virtual Device using a representative phone and stable system image.
Test more than one API level and screen size rather than treating one emulator as the Android ecosystem.
For a phone, enable Developer options and USB debugging, connect over USB or supported Wi-Fi pairing, and accept the host authorization prompt.
Use a data-capable cable and current SDK Platform Tools if the device does not appear.
Always test on real hardware before release; emulators cannot reproduce every vendor, sensor, thermal, camera, radio, and performance behavior.
7. Run from Android Studio
Choose the
apprun configuration.Select the AVD or authorized physical device in the target menu.
Click Run to compile, install the debug variant, and launch its activity.
Type a name and rotate the device to observe saveable UI state.
Open Logcat for the app process and inspect crashes or system messages.
Use Debug with breakpoints when you need to inspect control flow and values.
8. Verify the same build from the command line
./gradlew :app:assembleDebug :app:testDebugUnitTest :app:lintDebugBUILD SUCCESSFULThe wrapper is the reproducible entry point
./gradlewuses the Gradle version pinned in the project instead of an arbitrary system installation.assembleDebugcompiles and packages the debug APK.testDebugUnitTestruns local JVM tests for the debug variant.lintDebugperforms Android-specific static analysis and should be reviewed, not blindly silenced.Task names change with module names, product flavors, and custom variants; list tasks when necessary.
A clean build is not required before every invocation because Gradle tracks inputs and outputs.
9. Add a local unit test
package com.example.firstcomposeapp
import org.junit.Assert.assertEquals
import org.junit.Test
class GreetingTest {
@Test
fun blankNameUsesAndroidFallback() {
assertEquals("Hello, Android!", greetingFor(" "))
}
@Test
fun nameIsTrimmedBeforeFormatting() {
assertEquals("Hello, Ada!", greetingFor(" Ada "))
}
}Pure logic gives fast feedback
A local test runs on the development JVM and needs no emulator.
The tests document both blank-input fallback and whitespace normalization.
JUnit assertions compare exact outputs, making regressions obvious.
Compose behavior itself belongs in
androidTestwith Compose testing APIs and a device/emulator.Do not force Android framework code into local tests merely to avoid instrumentation; separate concerns instead.
10. Install through Gradle or ADB
adb devices -l
./gradlew :app:installDebugList of devices attached
emulator-5554 device ...
BUILD SUCCESSFULResolve the target before installing
adb devices -ldistinguishes authorized devices, offline devices, and emulators.With multiple targets, choose explicitly through Android Studio or ADB/Gradle-supported device selection rather than guessing.
installDebugbuilds and installs the debug variant but does not necessarily launch it.The debug certificate is for development and must never become the production signing identity.
Installing an app with the same application ID but an incompatible signing certificate fails until the conflict is resolved; uninstalling also removes that app’s local data.
Where the debug APK is written
find app/build/outputs/apk/debug -maxdepth 1 -type f -name '*.apk' -printapp/build/outputs/apk/debug/app-debug.apkAn APK is a build artifact, not the project
The conventional app-module debug output is
app/build/outputs/apk/debug/app-debug.apk; variants may produce different paths/names.Debug APKs are automatically signed with a development key and are suitable for testing.
The Android Studio Run action may produce a
testOnlyartifact intended for ADB installation.Generated output should not be committed to source control.
Rebuild from versioned source and pinned tooling rather than treating one APK as the source of truth.
Debugging the first failures
Gradle sync fails: read the first dependency, proxy, JDK, SDK, or plugin compatibility error; do not delete every cache immediately.
Preview is blank: compile the module, inspect preview errors, and confirm the composable has
@Previewplus a supported theme/context.No device appears: verify the emulator is booted or the phone is authorized, then update Platform Tools and check cable/USB rules.
App installs but crashes: filter Logcat to the app process and start at the first
FATAL EXCEPTIONcause.Changes do not appear: confirm the selected variant/device and whether Apply Changes could apply the edit; rerun when uncertain.
Text disappears after process death:
rememberSaveableis limited state restoration, not durable persistence; use ViewModel/SavedStateHandle or storage according to the data.Build succeeds only in Android Studio: compare the IDE Gradle JDK and environment with the wrapper build used in CI.
Permissions and privacy come later—only when needed
This greeting app needs no dangerous permissions, network access, analytics, or user account. That is a feature. Add a capability only when the product requires it, request runtime permission in context, explain the benefit, handle denial, minimize retained data, and update privacy disclosures and store declarations.
Debug APK versus release app bundle
The debug build is debuggable and signed with an SDK-generated debug key; it is not publishable as your production identity.
Google Play normally expects a signed Android App Bundle (
.aab) and generates optimized APKs for devices.A directly distributed APK is useful for testing or approved non-Play channels, but still needs a controlled release signature.
Protect the upload/release key in managed secret storage, restrict access, and never commit passwords or keystores to a public repository.
Set version code/name, test the release variant, inspect shrinking/obfuscation behavior, and preserve mapping/native symbols where applicable.
Use Android Studio’s Generate Signed Bundle/APK workflow or a secured CI release pipeline only after the app is ready.
A first-app completion checklist
Project syncs using the committed Gradle Wrapper.
The Compose preview renders in at least one theme/configuration.
The app runs on an emulator and a physical device.
State updates correctly and important configuration changes are tested.
Local tests and Android lint pass with reviewed output.
Logcat contains no crash, strict-mode, or repeated unexpected error from the app flow.
The debug APK can be reproduced from a clean checkout.
Application ID, minimum/target SDK strategy, and release signing ownership are documented before publication.
Accessibility, localization, privacy, offline/error states, and device diversity are part of the next iteration—not afterthoughts.
Official Android learning path
Create your first Android app is Google’s Kotlin and Compose Empty Activity codelab.
Build and run an app explains run configurations, targets, and debugging.
Run on the Android Emulator covers AVD creation and emulator behavior.
Run on a hardware device covers USB/Wi-Fi debugging and device testing.
Build from the command line documents wrapper tasks, APK paths, installation, and variants.
Build for release explains APK/app-bundle generation and signing boundaries.
Good next experiments
Move visible text into string resources and add a second locale.
Add a Compose UI test that types a name and verifies the greeting.
Introduce a ViewModel when state must survive beyond the screen or coordinate business logic.
Test the layout with large font scaling, dark theme, rotation, and a compact emulator.
Comments and corrections