Last updated: 23 October 2026. Rewritten for Kotlin and Jetpack Compose, which new Android Studio projects use by default.
In this tutorial you'll build a small but real Android app: a tap counter with a button, a reset option and a message that changes as the count grows. Along the way you'll learn how an Android project is organised, how Jetpack Compose draws a screen, and how state makes the screen update. It takes about 30 minutes.
I published my first app on Google Play, an MP4 to MP3 converter, back when Android apps were written in Java with XML layouts. New projects now use Kotlin and Jetpack Compose, which need much less code, so that's what this guide uses.
Before you start: install Android Studio and set up an emulator or your phone, using my guide to installing Android Studio on Windows and Ubuntu.
Contents
- Create the project
- A quick tour of the project
- Run the starter app
- Build the counter screen
- How state makes it work
- Make the count survive rotation
- Install it on your phone
- Common errors
- What to learn next
1. Create the project
- Open Android Studio and click New Project.
- With Phone and Tablet selected, choose Empty Activity and click Next. (This is the Jetpack Compose template.)
- Fill in:
- Name:
Tap Counter - Package name: leave the default, such as
com.example.tapcounter. It must be unique if you ever publish, so you'd changeexampleto your own name or domain. - Save location: note where it is.
- Minimum SDK: API 24 (Android 7.0). The dialog shows that this covers almost all active devices.
- Name:
- Click Finish and wait for the Gradle sync at the bottom of the window to finish. The first time can take several minutes.
2. A quick tour of the project
In the Project panel on the left (with the Android view selected), the files that matter are:
app/kotlin+java/com.example.tapcounter/MainActivity.kt: your app's code. You'll spend most of your time here.ui/theme/: colours, typography and the app theme.app/manifests/AndroidManifest.xml: the app's name, icon, permissions and which screen opens first.Gradle Scripts/build.gradle.kts (Module :app): settings such as minimum SDK, version number and libraries.
Open MainActivity.kt. The editor can show code, a visual preview, or both: click Split in the top-right corner of the editor.
3. Run the starter app
Pick your emulator or phone in the device menu at the top and click the green Run ▶ button. After the build, you'll see "Hello Android!" on the device. If that works, your setup is fine and you're ready to change the code.
4. Build the counter screen
Replace everything in MainActivity.kt below the package line with the code below. Change TapCounterTheme if Android Studio named your theme differently (check the name used in the original file).
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.tapcounter.ui.theme.TapCounterTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TapCounterTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
CounterScreen(modifier = Modifier.padding(innerPadding))
}
}
}
}
}
@Composable
fun CounterScreen(modifier: Modifier = Modifier) {
var count by rememberSaveable { mutableIntStateOf(0) }
val message = when {
count == 0 -> "Tap the button to start"
count < 10 -> "Keep going!"
count < 50 -> "Nice, $count taps"
else -> "Wow, you really like tapping"
}
Column(
modifier = modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "$count", style = MaterialTheme.typography.displayLarge)
Spacer(Modifier.height(8.dp))
Text(text = message, style = MaterialTheme.typography.bodyLarge)
Spacer(Modifier.height(32.dp))
Button(onClick = { count++ }) {
Text("Tap me")
}
Spacer(Modifier.height(8.dp))
OutlinedButton(onClick = { count = 0 }, enabled = count > 0) {
Text("Reset")
}
}
}
@Preview(showBackground = true)
@Composable
fun CounterScreenPreview() {
TapCounterTheme {
CounterScreen()
}
}
Run the app again. Tap the button: the number goes up and the message changes at 1, 10 and 50. Reset is greyed out until you've tapped at least once.
What each part does:
@Composablefunctions describe part of the screen.CounterScreenis your own composable, built from Compose'sColumn,Text,ButtonandSpacer.Columnstacks its children vertically.Rowwould place them side by side.Modifiersets size, padding and position:fillMaxSize()uses the whole screen,padding(24.dp)adds space around it.ScaffoldandinnerPaddingkeep your content clear of the status bar and navigation bar.@Previewshows the screen in Android Studio's Split/Design view without running the app. Click Build & Refresh if it doesn't update.
5. How state makes it work
The key line is:
var count by rememberSaveable { mutableIntStateOf(0) }
count is state: a value Compose watches. When count++ changes it, Compose re-runs CounterScreen and redraws only what changed. You never write "find this text view and update it", as you had to in the old Java and XML approach. You change the data, and the screen follows.
Try this: change rememberSaveable to just remember, run the app, tap a few times, then rotate the device. The count resets to 0. Rotating recreates the screen, and remember only keeps values while the screen exists. rememberSaveable saves the value and restores it, so the count survives. Change it back before continuing.
6. Make the count survive rotation, properly
rememberSaveable is fine for simple values. In real apps, data and logic belong in a ViewModel, which survives rotation and keeps the screen code simple. This is the standard Android architecture, and the next step on my Android roadmap. To try it, add the ViewModel Compose library to build.gradle.kts (Module :app) and sync. Check the Lifecycle release page for the current version:
dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:<latest-version>")
}
Then move the count into a ViewModel:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
class CounterViewModel : ViewModel() {
var count by mutableIntStateOf(0)
private set
fun increment() { count++ }
fun reset() { count = 0 }
}
@Composable
fun CounterScreen(
modifier: Modifier = Modifier,
vm: CounterViewModel = viewModel()
) {
val count = vm.count
// ...same UI as before, but:
// Button(onClick = { vm.increment() })
// OutlinedButton(onClick = { vm.reset() }, enabled = count > 0)
}
The screen now only displays data and forwards clicks; the ViewModel owns the logic. That separation is what interviewers mean when they ask about app architecture.
7. Install it on your phone
If your phone is connected with USB debugging on, choosing it in the device menu and clicking Run installs the app; it stays on the phone afterwards. To share it with a friend, use Build → Generate App Bundles or APKs → Generate APKs, then locate the debug APK in the notification and send that file. They'll need to allow installing from unknown sources.
Publishing on Google Play is a bigger step: a developer account, a signed release bundle (.aab), a store listing, and for new personal accounts a 14-day closed test with at least 12 testers. My Android roadmap covers it in stage 10.
8. Common errors
| Error | Fix |
|---|---|
Red Unresolved reference on a Compose name | A missing import. Click the red word and press Alt+Enter to import it. |
Unresolved reference: TapCounterTheme | Your theme has a different name. Check ui/theme/Theme.kt and use that name. |
Unresolved reference: viewModel | The lifecycle-viewmodel-compose dependency isn't added, or you haven't synced Gradle. |
| Preview says "render problem" or is blank | Build the project first (Build → Make Project), then Build & Refresh. |
| App crashes on launch | Open Logcat at the bottom, filter by your package name, and read the first red FATAL EXCEPTION line; it names the file and line. |
9. What to learn next
- Add a second screen and move between them with Navigation Compose.
- Save the count permanently with DataStore, so it survives closing the app.
- Turn it into a habit tracker with a list of habits and a Room database.
These are stages 4–6 of my Android developer roadmap, with official resources for each. If you get stuck, leave a comment with the exact error message from Logcat and I'll help.
Practising layouts? My free Android UI Kit app (my own) has ready-made UI examples you can study.