Pocket Studio Academy
HomePart 55.1

Dice Duel 1 — project setup and theme

Free lesson13 min read·4 questions

The first sitting of your first real app. You build the project from its foundations, choose a colour palette that will carry the whole game, draw a launcher icon out of shapes, and get a real APK with your own icon onto your phone.

Five sittings, one game

Everything up to now has been one idea at a time. A lesson on state. A lesson on . A lesson on animation. All of it real, none of it joined up.

Dice Duel joins it up. Two players share one phone. They take turns tapping a die. It tumbles, lands on a number, and that number is added to their score. First to 30 wins.

It takes five sittings:

ChapterWhat you add
1 (this one)The project, the palette, the icon, a screen you can see
2A die drawn by hand with Canvas
3Rolling, scoring, taking turns
4The throw animation
5Winning, a banner, and the last 10% of polish

Each chapter ends at a working you can install and show someone. That matters more than it sounds. An app that builds is a place you can stand.

This chapter builds nothing interactive at all. That is deliberate. Getting a real, signed, installable app with your own icon onto a real phone is the genuinely hard part of day one — harder than any game logic. Do it first, while you have the patience for it.

Think of it like this

Think about setting up a board game you have never played.

Before anyone rolls anything, somebody unfolds the board, puts the box lid where people can see the picture, and lays out the two player mats. Nothing has happened yet. Nobody has scored. But now the game exists on the table, and everyone can see where their bit goes.

That is chapter 1. The board, the lid, and two player mats reading zero. The dice come out of the box next time.

The bones of a project

A is a folder with a particular shape. reads that shape and turns it into an app. Here is Dice Duel's, with the files you will actually open marked:

text
1dice-duel/
2  settings.gradle.kts          <- which modules exist
3  build.gradle.kts             <- top-level plugin list
4  gradle.properties            <- build settings
5  gradle/
6    libs.versions.toml         <- every library and version
7    wrapper/                   <- pins the Gradle version
8  app/
9    build.gradle.kts           <- how the app is built
10    src/main/
11      AndroidManifest.xml      <- the app's ID card
12      res/                     <- icon, strings, window theme
13      java/com/nativeworks/diceduel/
14        MainActivity.kt
15        GameScreen.kt
16        ui/theme/Color.kt
17        ui/theme/Theme.kt

Four Kotlin files. That is the whole app by the end of chapter 5 — five, once the die gets its own file. Everything else is scaffolding you set up once and stop thinking about.

settings.gradle.kts

settings.gradle.ktskts
1// Which repositories Gradle may download from, and which
2// modules make up the build. Dice Duel has exactly one.
3pluginManagement {
4    repositories {
5        google()
6        mavenCentral()
7        gradlePluginPortal()
8    }
9}
10
11dependencyResolutionManagement {
12    repositories {
13        google()
14        mavenCentral()
15    }
16}
17
18rootProject.name = "Dice Duel"
19include(":app")

google() and mavenCentral() are — public warehouses of libraries. include(":app") says this build contains one module. Big apps have many; Dice Duel has one, forever.

The root build file

build.gradle.ktskts
1// Top-level build file. The plugins are named here but
2// switched off with "apply false"; the app module is the
3// one that actually turns them on.
4plugins {
5    alias(libs.plugins.android.application)
6    alias(libs.plugins.kotlin.android)
7    alias(libs.plugins.kotlin.compose)
8}

Three , all declared apply false. This file's only job is to say "these exist, at these versions". The module that needs them switches them on.

gradle.properties

gradle.propertiestext
1org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2android.useAndroidX=true
3android.nonTransitiveRClass=true
4kotlin.code.style=official

The first line is the one that matters on a phone: it caps the build at 2 GB of memory. Leave it alone unless a build actually runs out.

The version catalog

Every library and version lives in one file, and nowhere else:

gradle/libs.versions.tomltext
1[versions]
2agp = "8.7.3"
3kotlin = "2.1.0"
4composeBom = "2024.12.01"
5coreKtx = "1.15.0"
6lifecycle = "2.8.7"
7activityCompose = "1.9.3"
8
9[plugins]
10android-application = { id = "com.android.application", version.ref = "agp" }
11kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
12kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

This is the one file in the whole course whose lines run past 60 characters. TOML will not let a { ... } entry be split across lines, so it wraps in the editor and there is nothing to be done about it. It is also the file you are least likely to retype — the template generates most of it, and the download at the bottom of this lesson has the rest.

Note

The turns dashes into dots when you use it. The library entry androidx-ui-tooling-preview is written libs.androidx.ui.tooling.preview in a build file. Get that wrong and Gradle tells you Unresolved reference — it is in the Error Doctor below.

The app's build file

This is the one you will read most often.

The manifest

app/src/main/AndroidManifest.xmlxml
1<?xml version="1.0" encoding="utf-8"?>
2<manifest
3xmlns:android="http://schemas.android.com/apk/res/android">
4
5  <application
6    android:allowBackup="true"
7    android:icon="@mipmap/ic_launcher"
8    android:label="@string/app_name"
9    android:roundIcon="@mipmap/ic_launcher_round"
10    android:supportsRtl="true"
11    android:theme="@style/Theme.DiceDuel">
12
13    <activity
14      android:name=".MainActivity"
15      android:exported="true"
16      android:label="@string/app_name"
17      android:theme="@style/Theme.DiceDuel">
18
19      <intent-filter>
20        <action
21          android:name="android.intent.action.MAIN" />
22        <category
23          android:name="android.intent.category.LAUNCHER" />
24      </intent-filter>
25    </activity>
26  </application>
27
28</manifest>

Every @mipmap/..., @string/... and @style/... in the is a promise that a file or entry with that exact name exists. Spell one wrong and the build fails at resource linking, not at compile time — a different error, from a different tool, with a different vocabulary. Error Doctor, entry 4.

The is what puts the icon on the home screen. MAIN plus LAUNCHER means "this is a thing a person starts". Without it, the app installs and is completely unreachable.

The two res/values/themes.xml files (and their -night twin) set the colour Android paints behind your app in the fraction of a second before Compose takes over. Get it wrong and every cold start flashes white:

app/src/main/res/values/themes.xmlxml
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4  <!-- The window theme. Compose paints everything the
5       user sees, so this only needs to remove the old
6       action bar and set the colour behind the app
7       while it starts. -->
8  <style
9    name="Theme.DiceDuel"
10    parent="android:Theme.Material.Light.NoActionBar">
11    <item name="android:windowBackground">#FFF7F4FF</item>
12  </style>
13
14</resources>

The values-night copy is identical except for android:Theme.Material.NoActionBar and #FF121022. Android picks the folder; you never write an if.

The palette

Now the fun part. Every colour in the game is decided in one file, once.

ui/theme/Color.ktkotlin
1package com.nativeworks.diceduel.ui.theme
2
3import androidx.compose.ui.graphics.Color
4
5// The Dice Duel palette.
6//
7// Violet belongs to the game itself: the title, the
8// buttons, the pips. Rose belongs to player one and
9// teal belongs to player two, so the two sides never
10// get confused at a glance.
11//
12// Every colour is written as 0xAARRGGBB: alpha first,
13// then red, green and blue.
14
15val Violet = Color(0xFF5B3FD6)
16val VioletLight = Color(0xFFC3B2FF)
17val VioletPale = Color(0xFFE4DDFF)
18val VioletDeep = Color(0xFF422F9E)
19val VioletInk = Color(0xFF231152)
20
21val Rose = Color(0xFFD6355F)
22val RosePop = Color(0xFFFF87A6)
23val RosePale = Color(0xFFFFD9E2)
24val RoseDeep = Color(0xFF5C1730)
25val RoseInk = Color(0xFF5F0F27)
26
27val Teal = Color(0xFF0E7C6B)
28val TealPop = Color(0xFF5EE3CB)
29val TealPale = Color(0xFFB7F1E6)
30val TealDeep = Color(0xFF0B4A41)
31val TealInk = Color(0xFF00352C)
32
33val DayBg = Color(0xFFF7F4FF)
34val DayCard = Color(0xFFFFFFFF)
35val DayTint = Color(0xFFEBE5FA)
36val DayText = Color(0xFF1B1730)
37val DayMuted = Color(0xFF4A4463)
38val DayLine = Color(0xFFCFC6E8)
39
40val NightBg = Color(0xFF121022)
41val NightCard = Color(0xFF1C1930)
42val NightTint = Color(0xFF2A2545)
43val NightText = Color(0xFFEDE9FF)
44val NightMuted = Color(0xFFC6BFE0)
45val NightLine = Color(0xFF4A4368)
46
47// A real die is white with dark pips, in any light.
48val DieFaceLight = Color(0xFFFDFBFF)
49val DiePipDark = Color(0xFF241A4D)

Each colour is one number: 0xFF5B3FD6 is FF alpha (fully solid), 5B red, 3F green, D6 blue. The 0x prefix means "read the rest as hexadecimal".

The naming is doing real work. Rose is player one. Teal is player two. Violet is the game's own furniture — the title, the buttons, the on the die. Once a player has a colour, you never have to write the word "player 1" on a progress bar or a win banner; the colour says it.

The last two are outside the scheme on purpose. A real die is white with dark dots in any light, so DieFaceLight and DiePipDark do not flip in dark mode.

Wiring the palette into Material

does not want a bag of colours. It wants two with named slots, and then every component helps itself.

DarkColors is the same twenty lines with the night half of the palette:

ui/theme/Theme.ktkotlin
1private val DarkColors = darkColorScheme(
2    primary = VioletLight,
3    onPrimary = VioletInk,
4    primaryContainer = VioletDeep,
5    onPrimaryContainer = VioletPale,
6    secondary = RosePop,
7    onSecondary = RoseInk,
8    secondaryContainer = RoseDeep,
9    onSecondaryContainer = RosePale,
10    tertiary = TealPop,
11    onTertiary = TealInk,
12    tertiaryContainer = TealDeep,
13    onTertiaryContainer = TealPale,
14    background = NightBg,
15    onBackground = NightText,
16    surface = NightCard,
17    onSurface = NightText,
18    surfaceVariant = NightTint,
19    onSurfaceVariant = NightMuted,
20    outline = NightLine
21)

Notice it is not an inversion. Violet does not become anti-violet; it becomes a lighter violet, because a dark screen needs bright accents and a light screen needs deep ones. That is the whole trick of a good .

Then one composable picks between them:

ui/theme/Theme.ktkotlin
1@Composable
2fun DiceDuelTheme(
3    darkTheme: Boolean = isSystemInDarkTheme(),
4    content: @Composable () -> Unit
5) {
6    val colors = if (darkTheme) DarkColors else LightColors
7    MaterialTheme(
8        colorScheme = colors,
9        content = content
10    )
11}

isSystemInDarkTheme() reads the phone's setting, and because it is a default you can still force either mode when you want one — handy for a @Preview. content is everything wrapped inside; that is what makes DiceDuelTheme { ... } work.

The Activity

MainActivity.ktkotlin
1package com.nativeworks.diceduel
2
3import android.os.Bundle
4import androidx.activity.ComponentActivity
5import androidx.activity.compose.setContent
6import com.nativeworks.diceduel.ui.theme.DiceDuelTheme
7
8// The single Activity. Its only job is to hand the
9// screen over to Compose.
10class MainActivity : ComponentActivity() {
11    override fun onCreate(state: Bundle?) {
12        super.onCreate(state)
13        setContent {
14            DiceDuelTheme {
15                GameScreen()
16            }
17        }
18    }
19}

Nineteen lines, and this file will not change again for the rest of Dice Duel. One , one theme, one screen.

The screen

TitleBlock is two Text calls and a :

GameScreen.ktkotlin
1// The name of the game, plus the one rule you need.
2@Composable
3private fun TitleBlock() {
4    val scheme = MaterialTheme.colorScheme
5    Text(
6        text = "DICE DUEL",
7        color = scheme.primary,
8        fontSize = 40.sp,
9        fontWeight = FontWeight.Black,
10        letterSpacing = 6.sp
11    )
12    Spacer(Modifier.height(2.dp))
13    Text(
14        text = "First to 30 wins the duel",
15        color = scheme.onSurfaceVariant,
16        fontSize = 15.sp,
17        fontWeight = FontWeight.Medium,
18        letterSpacing = 1.sp
19    )
20}

letterSpacing = 6.sp is most of what makes that title look designed rather than typed. Heavy weight plus wide spacing reads as a logo. Try it at 0.sp once, just to see.

ScoreBoard puts two panels in a and shares the width equally:

GameScreen.ktkotlin
1// Two panels side by side, one per player.
2@Composable
3private fun ScoreBoard(p1: Int, p2: Int, turn: Int) {
4    val scheme = MaterialTheme.colorScheme
5    Row(
6        modifier = Modifier.fillMaxWidth(),
7        horizontalArrangement = Arrangement.spacedBy(14.dp)
8    ) {
9        PlayerPanel(
10            name = "PLAYER 1",
11            score = p1,
12            accent = scheme.secondary,
13            panel = scheme.secondaryContainer,
14            label = scheme.onSecondaryContainer,
15            active = turn == 1,
16            modifier = Modifier.weight(1f)
17        )
18        PlayerPanel(
19            name = "PLAYER 2",
20            score = p2,
21            accent = scheme.tertiary,
22            panel = scheme.tertiaryContainer,
23            label = scheme.onTertiaryContainer,
24            active = turn == 2,
25            modifier = Modifier.weight(1f)
26        )
27    }
28}

Both panels get Modifier.weight(1f), so they split whatever is left after the 14dp gutter. Equal mean equal widths, no matter how wide the phone is.

And the panel itself — one composable, drawn twice, in two different colours:

An icon made of shapes

No PNG files anywhere in this project — including the launcher icon. It is an : a background layer and a foreground layer, which the launcher masks into whatever shape the phone uses.

The background is one colour:

res/values/ic_launcher_background.xmlxml
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3  <color name="ic_launcher_background">#FF5B3FD6</color>
4</resources>

The foreground is a — a white die, tilted 14 degrees, showing a five, with a soft shadow behind it. Three paths, all written as :

res/drawable/ic_launcher_foreground.xmlxml
1<?xml version="1.0" encoding="utf-8"?>
2<vector
3xmlns:android="http://schemas.android.com/apk/res/android"
4    android:width="108dp"
5    android:height="108dp"
6    android:viewportWidth="108"
7    android:viewportHeight="108">
8
9  <!-- One white die, tilted, showing a five. Drawn with
10       vector paths so it stays sharp on every screen and
11       adds no PNG files to the app. -->
12  <group
13    android:pivotX="54"
14    android:pivotY="54"
15    android:rotation="-14">
16
17    <!-- Soft shadow, the same shape nudged down. -->
18    <group
19      android:translateX="2"
20      android:translateY="4">
21      <path
22        android:fillColor="#33150C3F"
23        android:pathData="M40,30 H68 A10,10 0 0 1 78,40
24          V68 A10,10 0 0 1 68,78 H40 A10,10 0 0 1 30,68
25          V40 A10,10 0 0 1 40,30 Z" />
26    </group>
27
28    <!-- The die face. -->
29    <path
30      android:fillColor="#FFFDFBFF"
31      android:pathData="M40,30 H68 A10,10 0 0 1 78,40
32        V68 A10,10 0 0 1 68,78 H40 A10,10 0 0 1 30,68
33        V40 A10,10 0 0 1 40,30 Z" />
34
35    <!-- Five pips, all in one path. -->
36    <path
37      android:fillColor="#FF241A4D"
38      android:pathData="M36.5,42 a5.5,5.5 0 1,0 11,0
39        a5.5,5.5 0 1,0 -11,0 Z
40        M60.5,42 a5.5,5.5 0 1,0 11,0
41        a5.5,5.5 0 1,0 -11,0 Z
42        M48.5,54 a5.5,5.5 0 1,0 11,0
43        a5.5,5.5 0 1,0 -11,0 Z
44        M36.5,66 a5.5,5.5 0 1,0 11,0
45        a5.5,5.5 0 1,0 -11,0 Z
46        M60.5,66 a5.5,5.5 0 1,0 11,0
47        a5.5,5.5 0 1,0 -11,0 Z" />
48  </group>
49</vector>

Read the die-face path once and it stops being frightening. M40,30 moves the pen to x 40, y 30. H68 draws a horizontal line to x 68. A10,10 0 0 1 78,40 draws an arc of radius 10 — that is a rounded corner. Four lines and four arcs, then Z closes the loop. The shadow is the identical path in translucent dark violet, nudged 2 right and 4 down.

The <group> wrapping everything rotates the lot by -14 degrees around the centre point 54,54. Rotating a group is far easier than working out five rotated pip positions by hand.

Both mipmap-anydpi-v26/ic_launcher.xml and ic_launcher_round.xml are the same three lines:

res/mipmap-anydpi-v26/ic_launcher.xmlxml
1<?xml version="1.0" encoding="utf-8"?>
2<adaptive-icon
3xmlns:android="http://schemas.android.com/apk/res/android">
4  <background
5    android:drawable="@color/ic_launcher_background" />
6  <foreground
7    android:drawable="@drawable/ic_launcher_foreground" />
8  <monochrome
9    android:drawable="@drawable/ic_launcher_foreground" />
10</adaptive-icon>

The third layer is the one, which Android 13 and newer uses for themed icons — your die, tinted to match the user's wallpaper. Reusing the foreground for it is free and takes one line.

What you get

9:41▲ ▮
DICE DUEL
First to 30 wins the duel
PLAYER 1
0
PLAYER 2
0

End of chapter 1. Nothing is interactive — and that is the point.

Player 1's panel has the rose ring because turn is 1. Player 2's ring is there too — it is just transparent. Everything below the panels is empty, and that gap is exactly where the die goes next time.

Try it in Pocket Studio
  1. Open Pocket Studio and tap Projects, then New Project.
  2. Choose the Empty Compose Activity template.
  3. Set the name to Dice Duel and the package to com.nativeworks.diceduel. Tap Create.
  4. Wait for the first . It downloads Gradle and the Compose libraries, so it is the slow one — several minutes on a first run.
  5. Open app/build.gradle.kts and make it match the file in this lesson. Check minSdk is 26 and that all three alias(...) plugin lines are present.
  6. Open gradle/libs.versions.toml and check every version matches the catalog above.
  7. In the file tree, long-press the ui folder — or java/com/nativeworks/diceduel if there is no ui folder yet — and tap New → Package. Name it theme.
  8. In ui/theme, tap New → Kotlin File twice. Name them Color and Theme. Type the two files from this lesson into them.
  9. Open MainActivity.kt. Delete whatever the template generated below onCreate and replace the body with the version above.
  10. Tap New → Kotlin File beside MainActivity.kt, name it GameScreen, and type GameScreen.kt. Accept every import Pocket Studio offers.
  11. Open app/src/main/res and add the four resource files: values/strings.xml, values/themes.xml, values-night/themes.xml, values/ic_launcher_background.xml.
  12. Add drawable/ic_launcher_foreground.xml and the two mipmap-anydpi-v26 files. Delete any ic_launcher PNGs the template created, in every mipmap-* folder.
  13. Open AndroidManifest.xml and make it match.
  14. Tap Run. When the build finishes, accept the install prompt.
  15. Press Home and find Dice Duel on your home screen. Your violet die icon should be there.
  16. Switch your phone to dark mode and open the app again. The whole screen should change.
Error Doctor5 common errors
> SDK location not found. Define a valid SDK location with an ANDROID_HOME environment variable or by setting the sdk.dir path in your project's local properties file at '...\dice-duel\local.properties'.
MeansGradle has no idea where the Android SDK lives, so it cannot even work out what :app:compileDebugJavaWithJavac depends on.
FixPocket Studio sets ANDROID_HOME for you, so this usually means the project was copied from a computer. Do not create a local.properties file to fix it — that hard-codes one machine's folder path and breaks the project everywhere else. Delete local.properties if it exists and rebuild.
> Starting in Kotlin 2.0, the Compose Compiler Gradle plugin is required when compose is enabled. See the following link for more information: https://d.android.com/r/studio-ui/compose-compiler
MeansYou wrote buildFeatures { compose = true } but did not add the plugin that actually compiles @Composable functions. Turning the feature on is only half of it.
FixAdd alias(libs.plugins.kotlin.compose) to app/build.gradle.kts, and the matching line with apply false in the root build.gradle.kts. Both files, or it will not resolve.
Line 47: implementation(libs.androidx.material) ^ Unresolved reference: material
MeansThe libs. names are generated from gradle/libs.versions.toml. There is no androidx-material entry in the catalog, so there is no libs.androidx.material to use.
FixUse the name that exists: libs.androidx.material3. Remember that dashes in the catalog become dots in Kotlin — androidx-ui-tooling-preview is written libs.androidx.ui.tooling.preview.
> Android resource linking failed ERROR: AndroidManifest.xml:5:3-26:17: AAPT: error: resource mipmap/ic_launchr (aka com.nativeworks.diceduel:mipmap/ic_launchr) not found.
MeansEvery @mipmap/..., @string/... and @style/... in the manifest must match a real file or entry, spelled exactly. This error comes from AAPT, the resource compiler, not from Kotlin — which is why it looks so different from the errors you are used to.
FixHere ic_launchr is missing an e; the file is res/mipmap-anydpi-v26/ic_launcher.xml. The same error for string/app_name means res/values/strings.xml is missing or does not contain that entry.
e: GameScreen.kt:181:5 None of the following candidates is applicable: fun Text(text: String, modifier: Modifier = ..., color: Color = ..., fontSize: TextUnit = ..., ...): Unit
MeansYou wrote fontSize = 40. Compose measures text in TextUnit and layout in Dp, never in bare numbers, so Kotlin dumps every Text overload to show that none of them accepts an Int there.
FixWrite fontSize = 40.sp and add import androidx.compose.ui.unit.sp. The same rule catches height(26) — that one wants 26.dp.

Dice Duel — end of chapter 1

A complete project. Unzip it, open it in Pocket Studio, and press Run.

Download ZIP
Recap
  • A Dice Duel is four Kotlin files plus scaffolding you set up once: settings.gradle.kts, the two build files, the , the and a handful of resources.
  • Eight , one of them a that pins all of Compose together. Fewer dependencies means faster builds, and on a phone you notice.
  • The palette lives in one file as numbers, and is wired into two Material 3 . Player 1 is the secondary slot, player 2 is tertiary, and the game's own furniture is primary — so no game code ever names a colour.
  • Modifier order is not decoration. fillMaxSize before background; clip before background before border; insets before your own padding.
  • The launcher icon is an built from a — three paths and a rotated group, no image files at all.
  • Next: the die. You will draw a real one on a — a rounded square and up to six placed on a 3×3 grid, sharp at any size, costing the app zero kilobytes.