# radiOLA Bootstrap Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Bootstrap a new Android Gradle project with Kotlin, Jetpack Compose, Hilt, Retrofit, Room, ExoPlayer, and all required dependencies for the radiOLA radio streaming app. **Architecture:** Single Gradle module with Clean Architecture packages (`ui/`, `domain/`, `data/`, `service/`, `deeplink/`, `widget/`). Dependencies managed via `libs.versions.toml`. Hilt for DI. Jetpack Navigation Compose for routing. **Tech Stack:** Kotlin 2.0, Jetpack Compose BOM, Hilt, Retrofit + Kotlinx Serialization, Room, ExoPlayer (Media3), DataStore, Coil, JUnit 5 + MockK + Turbine --- ## File Structure ``` app/ ├── build.gradle.kts ├── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/com/radiola/ │ │ │ ├── MainActivity.kt │ │ │ ├── ui/ │ │ │ │ ├── theme/ │ │ │ │ │ ├── Color.kt │ │ │ │ │ ├── Theme.kt │ │ │ │ │ └── Type.kt │ │ │ │ ├── player/ │ │ │ │ ├── stations/ │ │ │ │ ├── favorites/ │ │ │ │ ├── history/ │ │ │ │ ├── settings/ │ │ │ │ └── navigation/ │ │ │ ├── domain/ │ │ │ │ ├── model/ │ │ │ │ ├── repository/ │ │ │ │ └── usecase/ │ │ │ ├── data/ │ │ │ │ ├── remote/ │ │ │ │ ├── local/ │ │ │ │ └── repository/ │ │ │ ├── service/ │ │ │ ├── deeplink/ │ │ │ └── widget/ │ │ └── res/ │ │ ├── values/ │ │ └── xml/ │ └── test/ │ └── java/com/radiola/ ├── proguard-rules.pro gradle/ ├── libs.versions.toml build.gradle.kts settings.gradle.kts gradle.properties ``` --- ## Task 1: Root Gradle Wrapper & Settings **Files:** - Create: `settings.gradle.kts` - Create: `build.gradle.kts` (root) - Create: `gradle.properties` - Create: `gradle/libs.versions.toml` - [ ] **Step 1: Write `settings.gradle.kts`** ```kotlin pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } } rootProject.name = "radiOLA" include(":app") ``` - [ ] **Step 2: Write root `build.gradle.kts`** ```kotlin plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.hilt) apply false } ``` - [ ] **Step 3: Write `gradle.properties`** ```properties org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 android.useAndroidX=true kotlin.code.style=official android.nonTransitiveRClass=true ``` - [ ] **Step 4: Write `gradle/libs.versions.toml`** ```toml [versions] agp = "8.5.0" kotlin = "2.0.0" coreKtx = "1.13.1" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" lifecycleRuntimeKtx = "2.8.2" activityCompose = "1.9.0" composeBom = "2024.06.00" hilt = "2.51.1" ksp = "2.0.0-1.0.22" retrofit = "2.11.0" serialization = "1.6.3" room = "2.6.1" datastore = "1.1.1" media3 = "1.3.1" coil = "2.6.0" navigation = "2.7.7" mockk = "1.13.11" turbine = "1.1.0" coroutinesTest = "1.8.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version = "1.2.0" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" } retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-converter-kotlinx-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version = "4.12.0" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" } datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" } coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" } turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutinesTest" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } ``` - [ ] **Step 5: Run Gradle sync check** Run: `./gradlew tasks --dry-run` Expected: No errors, dry-run completes successfully. - [ ] **Step 6: Commit** ```bash git add gradle.properties settings.gradle.kts build.gradle.kts gradle/libs.versions.toml git commit -m "chore: bootstrap root Gradle with version catalog" ``` --- ## Task 2: App Module build.gradle.kts & Manifest **Files:** - Create: `app/build.gradle.kts` - Create: `app/proguard-rules.pro` - Create: `app/src/main/AndroidManifest.xml` - [ ] **Step 1: Write `app/build.gradle.kts`** ```kotlin plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) alias(libs.plugins.hilt) } android { namespace = "com.radiola" compileSdk = 34 defaultConfig { applicationId = "com.radiola" minSdk = 26 targetSdk = 34 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = "17" } buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.14" } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } testOptions { unitTests { isReturnDefaultValues = true isIncludeAndroidResources = true } } } dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.activity.compose) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.navigation.compose) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.hilt.android) ksp(libs.hilt.compiler) androidTestImplementation(libs.hilt.android.testing) kspAndroidTest(libs.hilt.compiler) implementation(libs.retrofit) implementation(libs.retrofit.converter.kotlinx.serialization) implementation(libs.kotlinx.serialization.json) implementation(libs.okhttp.logging) implementation(libs.room.runtime) implementation(libs.room.ktx) ksp(libs.room.compiler) testImplementation(libs.room.testing) implementation(libs.datastore.preferences) implementation(libs.media3.exoplayer) implementation(libs.media3.session) implementation(libs.coil.compose) testImplementation(libs.junit) testImplementation(libs.mockk) testImplementation(libs.turbine) testImplementation(libs.kotlinx.coroutines.test) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) } ``` - [ ] **Step 2: Write `app/proguard-rules.pro`** ```proguard # Keep DataStore serializer -keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite { ; } # Keep kotlinx.serialization -keepattributes *Annotation*, InnerClasses -dontnote kotlinx.serialization.AnnotationsKt ``` - [ ] **Step 3: Write `app/src/main/AndroidManifest.xml`** ```xml ``` - [ ] **Step 4: Commit** ```bash git add app/ git commit -m "chore: add app module build config and manifest" ``` --- ## Task 3: Application Class & Basic Theme **Files:** - Create: `app/src/main/java/com/radiola/RadiolaApplication.kt` - Create: `app/src/main/java/com/radiola/MainActivity.kt` - Create: `app/src/main/java/com/radiola/ui/theme/Color.kt` - Create: `app/src/main/java/com/radiola/ui/theme/Theme.kt` - Create: `app/src/main/java/com/radiola/ui/theme/Type.kt` - Create: `app/src/main/res/values/strings.xml` - Create: `app/src/main/res/values/themes.xml` - Create: `app/src/main/res/values/colors.xml` - Create: `app/src/main/res/xml/data_extraction_rules.xml` - Create: `app/src/main/res/xml/backup_rules.xml` - [ ] **Step 1: Write `RadiolaApplication.kt`** ```kotlin package com.radiola import android.app.Application import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp class RadiolaApplication : Application() ``` - [ ] **Step 2: Write `MainActivity.kt`** ```kotlin package com.radiola import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import com.radiola.ui.theme.RadiolaTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { RadiolaTheme { // TODO: Navigation will go here } } } } ``` - [ ] **Step 3: Write theme files (Color.kt, Theme.kt, Type.kt)** `Color.kt`: ```kotlin package com.radiola.ui.theme import androidx.compose.ui.graphics.Color val Primary = Color(0xFF6200EE) val PrimaryDark = Color(0xFF3700B3) val Secondary = Color(0xFF03DAC6) val Background = Color(0xFF121212) val Surface = Color(0xFF1E1E1E) val OnPrimary = Color.White val OnSecondary = Color.Black val OnBackground = Color.White val OnSurface = Color.White ``` `Theme.kt`: ```kotlin package com.radiola.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable private val DarkColorScheme = darkColorScheme( primary = Primary, secondary = Secondary, background = Background, surface = Surface, onPrimary = OnPrimary, onSecondary = OnSecondary, onBackground = OnBackground, onSurface = OnSurface ) private val LightColorScheme = lightColorScheme( primary = Primary, secondary = Secondary, onPrimary = OnPrimary, onSecondary = OnSecondary ) @Composable fun RadiolaTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme MaterialTheme( colorScheme = colorScheme, typography = Typography, content = content ) } ``` `Type.kt`: ```kotlin package com.radiola.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp val Typography = Typography( headlineLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 28.sp ), headlineMedium = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.SemiBold, fontSize = 22.sp ), titleMedium = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 16.sp ), bodyMedium = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 14.sp ), labelMedium = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 12.sp ) ) ``` - [ ] **Step 4: Write resource files** `strings.xml`: ```xml radiOLA Радио Избранное История Настройки Offline mode Play Pause ``` `themes.xml`: ```xml