From cc-mobile-android
Authoritative blueprint for scaffolding a brand-new Android app with this project's conventions. Used by /init-android-app. Contains every file template, placeholder list, feature-flag block, and the procedure to emit a runnable splash.
How this skill is triggered — by the user, by Claude, or both
Slash command
/cc-mobile-android:android-app-skeletonThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This file is the template registry. The `/init-android-app` command reads this skill top-to-bottom and substitutes placeholders before writing files. Do not improvise: the whole point of the skeleton is reproducibility.
This file is the template registry. The /init-android-app command reads this skill top-to-bottom and substitutes placeholders before writing files. Do not improvise: the whole point of the skeleton is reproducibility.
| Placeholder | Meaning | Example |
|---|---|---|
{{APP_NAME}} | Gradle project folder name, lowercase kebab or snake | my_app |
{{APP_CLASS}} | Application subclass name in PascalCase | MyApp |
{{PACKAGE_ID}} | applicationId / root package | com.example.myapp |
{{PACKAGE_PATH}} | slash form of package id | com/example/myapp |
{{APP_DISPLAY_NAME}} | Human-facing name | My App |
| Flag | Adds |
|---|---|
INCLUDE_ROOM | Room module + entities/DAO/DB, migration skeleton, schema export |
INCLUDE_DATASTORE | DataStore module + typed prefs |
INCLUDE_FIREBASE | google-services plugin + Crashlytics + Analytics init |
{{APP_NAME}}/ with Gradle wrapper.settings.gradle.kts, root build.gradle.kts, gradle.properties, gradle/libs.versions.toml.build.gradle.kts files (app/, core/domain/, core/data/).AndroidManifest.xml, strings.xml, themes, launcher icons.:core:domain (Outcome, DomainError, sample use case contract).:core:data (Retrofit/OkHttp factory, remote data source, Hilt module). Add Room / DataStore files behind flags.:app (Application, MainActivity, theme, splash screen + VM, nav graph).:app tests (splash VM test).settings.gradle.ktspluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
google()
mavenCentral()
}
}
rootProject.name = "{{APP_NAME}}"
include(":app")
include(":core:domain")
include(":core:data")
build.gradle.kts (root)plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.hilt) apply false
// INCLUDE_FIREBASE: alias(libs.plugins.google.services) apply false
// INCLUDE_FIREBASE: alias(libs.plugins.firebase.crashlytics) apply false
}
gradle.propertiesorg.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
kotlin.code.style=official
android.useAndroidX=true
android.nonTransitiveRClass=true
gradle/libs.versions.tomlDo not copy the version strings below verbatim. The [versions] block below shows the shape — which version refs the scaffold uses. When generating the file for a new project, resolve each version to the latest stable that satisfies the floor-constraint table further down, then write those into the file.
In practice: open the Gradle Plugin Portal / Maven Central for each [versions] key, take the newest non-alpha/non-RC release, and substitute it in. If a resolved version falls below its listed floor, stop and surface the blocker — something is pinning the project below the line the rest of the skill assumes.
[versions]
agp = "<latest-stable>"
kotlin = "<latest-stable>"
ksp = "<kotlin>-<ksp-patch>" # must match the resolved kotlin version
coroutines = "<latest-stable>"
hilt = "<latest-stable>"
hilt-navigation-compose = "<latest-stable>"
compose-bom = "<latest-stable>"
navigation-compose = "<latest-stable>"
lifecycle = "<latest-stable>"
activity-compose = "<latest-stable>"
androidx-core-ktx = "<latest-stable>"
retrofit = "<latest-stable>"
retrofit-kotlinx-serialization = "<latest-stable>"
okhttp = "<latest-stable>"
kotlinx-serialization = "<latest-stable>"
datastore = "<latest-stable>"
room = "<latest-stable>"
firebase-bom = "<latest-stable>"
junit = "<latest-stable>"
mockk = "<latest-stable>"
turbine = "<latest-stable>"
coroutines-test = "<latest-stable>"
[libraries]
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
compose-ui = { module = "androidx.compose.ui:ui" }
compose-material3 = { module = "androidx.compose.material3:material3" }
compose-tooling = { module = "androidx.compose.ui:ui-tooling" }
compose-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-kotlinx-serialization = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit-kotlinx-serialization" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
# INCLUDE_ROOM
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
# INCLUDE_DATASTORE
datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
# INCLUDE_FIREBASE
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" }
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" }
firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" }
junit = { module = "junit:junit", version.ref = "junit" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines-test" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", 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" }
# INCLUDE_FIREBASE
google-services = { id = "com.google.gms.google-services", version = "<latest-stable>" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "<latest-stable>" }
Floor constraints (enforce before writing the file):
| Ref | Floor | Reason |
|---|---|---|
agp | >= 8.5.0 | K2 Kotlin plugin + Compose Compiler Gradle plugin both need AGP 8.5+. |
kotlin | >= 2.0.0 | K2 compiler + the org.jetbrains.kotlin.plugin.compose plugin; the rest of the skill assumes K2 diagnostics. |
ksp | matches kotlin patch version (e.g. 2.0.21-1.0.28) | KSP versioning is <kotlinVersion>-<kspPatch>; misalignment is the #1 cause of broken annotation processing. |
hilt | >= 2.51 | First version with official KSP support — the skill uses ksp(libs.hilt.compiler), not kapt. |
compose-bom | >= 2024.09.00 | Material 3 APIs the scaffold uses assume late-2024 BOM. |
room | >= 2.6.0 | KSP support; the skill uses ksp(libs.room.compiler). |
coroutines | >= 1.8.0 | Dispatchers.setMain in tests + structured cancellation guarantees the skill relies on. |
activity-compose | >= 1.9.0 | Predictive-back integration. |
If any resolved version falls below its floor, stop and report. Don't silently downgrade the template.
:app moduleapp/build.gradle.ktsplugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
// INCLUDE_FIREBASE: alias(libs.plugins.google.services)
// INCLUDE_FIREBASE: alias(libs.plugins.firebase.crashlytics)
}
android {
namespace = "{{PACKAGE_ID}}"
compileSdk = 35
defaultConfig {
applicationId = "{{PACKAGE_ID}}"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "0.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { useSupportLibrary = true }
}
flavorDimensions += "env"
productFlavors {
create("dev") {
dimension = "env"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
}
create("prod") {
dimension = "env"
}
}
buildTypes {
debug {
isMinifyEnabled = false
}
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions { jvmTarget = "17" }
buildFeatures { compose = true }
packaging {
resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
dependencies {
implementation(project(":core:domain"))
implementation(project(":core:data"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.material3)
debugImplementation(libs.compose.tooling)
implementation(libs.compose.tooling.preview)
implementation(libs.navigation.compose)
implementation(libs.kotlinx.serialization.json)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
implementation(libs.hilt.navigation.compose)
// INCLUDE_FIREBASE
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.crashlytics)
implementation(libs.firebase.analytics)
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.turbine)
testImplementation(libs.kotlinx.coroutines.test)
}
app/src/main/AndroidManifest.xml<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".{{APP_CLASS}}"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.App">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.App">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
app/src/main/res/values/strings.xml<resources>
<string name="app_name">{{APP_DISPLAY_NAME}}</string>
</resources>
app/src/main/res/values/themes.xml<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.App" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
app/src/main/java/{{PACKAGE_PATH}}/{{APP_CLASS}}.ktpackage {{PACKAGE_ID}}
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class {{APP_CLASS}} : Application()
app/src/main/java/{{PACKAGE_PATH}}/MainActivity.ktpackage {{PACKAGE_ID}}
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import {{PACKAGE_ID}}.navigation.AppNavGraph
import {{PACKAGE_ID}}.ui.theme.AppTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AppTheme {
AppNavGraph()
}
}
}
}
app/src/main/java/{{PACKAGE_PATH}}/ui/theme/AppTheme.ktpackage {{PACKAGE_ID}}.ui.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.foundation.isSystemInDarkTheme
@Composable
fun AppTheme(content: @Composable () -> Unit) {
val context = LocalContext.current
val scheme = if (isSystemInDarkTheme()) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
MaterialTheme(colorScheme = scheme, content = content)
}
app/src/main/java/{{PACKAGE_PATH}}/navigation/AppNavGraph.ktpackage {{PACKAGE_ID}}.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.serialization.Serializable
import {{PACKAGE_ID}}.ui.splash.SplashScreen
@Serializable data object Splash
@Composable
fun AppNavGraph() {
val nav = rememberNavController()
NavHost(nav, startDestination = Splash) {
composable<Splash> { SplashScreen() }
}
}
app/src/main/java/{{PACKAGE_PATH}}/ui/splash/SplashScreen.ktpackage {{PACKAGE_ID}}.ui.splash
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun SplashScreen(vm: SplashViewModel = hiltViewModel()) {
val state by vm.state.collectAsStateWithLifecycle()
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(state.message)
}
}
@Preview
@Composable
private fun SplashPreview() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("{{APP_DISPLAY_NAME}}")
}
}
app/src/main/java/{{PACKAGE_PATH}}/ui/splash/SplashViewModel.ktpackage {{PACKAGE_ID}}.ui.splash
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
data class SplashState(val message: String = "{{APP_DISPLAY_NAME}}")
@HiltViewModel
class SplashViewModel @Inject constructor() : ViewModel() {
private val _state = MutableStateFlow(SplashState())
val state: StateFlow<SplashState> = _state.asStateFlow()
}
app/src/test/java/{{PACKAGE_PATH}}/ui/splash/SplashViewModelTest.ktpackage {{PACKAGE_ID}}.ui.splash
import app.cash.turbine.test
import org.junit.Test
import kotlin.test.assertEquals
class SplashViewModelTest {
@Test
fun `emits initial state with app display name`() = kotlinx.coroutines.test.runTest {
val vm = SplashViewModel()
vm.state.test {
assertEquals("{{APP_DISPLAY_NAME}}", awaitItem().message)
cancelAndIgnoreRemainingEvents()
}
}
}
:core:domain modulecore/domain/build.gradle.ktsplugins {
alias(libs.plugins.kotlin.jvm)
}
dependencies {
implementation(libs.kotlinx.coroutines.core)
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
}
core/domain/src/main/kotlin/{{PACKAGE_PATH}}/core/domain/Outcome.ktpackage {{PACKAGE_ID}}.core.domain
sealed interface Outcome<out T> {
data class Success<T>(val value: T) : Outcome<T>
data class Failure(val error: DomainError) : Outcome<Nothing>
}
inline fun <T, R> Outcome<T>.map(block: (T) -> R): Outcome<R> = when (this) {
is Outcome.Success -> Outcome.Success(block(value))
is Outcome.Failure -> this
}
core/domain/src/main/kotlin/{{PACKAGE_PATH}}/core/domain/DomainError.ktpackage {{PACKAGE_ID}}.core.domain
sealed class DomainError(open val cause: Throwable? = null) {
data class Network(override val cause: Throwable? = null) : DomainError(cause)
data class Unauthorized(override val cause: Throwable? = null) : DomainError(cause)
data class NotFound(override val cause: Throwable? = null) : DomainError(cause)
data class Server(val code: Int, override val cause: Throwable? = null) : DomainError(cause)
data class Unknown(override val cause: Throwable? = null) : DomainError(cause)
}
:core:data modulecore/data/build.gradle.ktsplugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
}
android {
namespace = "{{PACKAGE_ID}}.core.data"
compileSdk = 35
defaultConfig { minSdk = 26 }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions { jvmTarget = "17" }
}
dependencies {
implementation(project(":core:domain"))
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.serialization.json)
implementation(libs.retrofit)
implementation(libs.retrofit.kotlinx.serialization)
implementation(libs.okhttp)
implementation(libs.okhttp.logging)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
// INCLUDE_ROOM
implementation(libs.room.runtime)
implementation(libs.room.ktx)
ksp(libs.room.compiler)
// INCLUDE_DATASTORE
implementation(libs.datastore.preferences)
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.kotlinx.coroutines.test)
}
core/data/src/main/kotlin/{{PACKAGE_PATH}}/core/data/network/ApiClientFactory.ktpackage {{PACKAGE_ID}}.core.data.network
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
import java.util.concurrent.TimeUnit
object ApiClientFactory {
fun retrofit(baseUrl: String): Retrofit {
val client = OkHttpClient.Builder()
.callTimeout(30, TimeUnit.SECONDS)
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC
})
.build()
val json = Json { ignoreUnknownKeys = true; explicitNulls = false }
return Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
}
}
core/data/src/main/kotlin/{{PACKAGE_PATH}}/core/data/di/DataModule.ktpackage {{PACKAGE_ID}}.core.data.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import {{PACKAGE_ID}}.core.data.network.ApiClientFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides @Singleton
fun retrofit(): Retrofit = ApiClientFactory.retrofit("https://example.invalid/")
}
Only emit when INCLUDE_ROOM is true.
core/data/src/main/kotlin/{{PACKAGE_PATH}}/core/data/persistence/AppDatabase.ktpackage {{PACKAGE_ID}}.core.data.persistence
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(
entities = [SampleEntity::class],
version = 1,
exportSchema = true,
)
abstract class AppDatabase : RoomDatabase() {
abstract fun sampleDao(): SampleDao
}
core/data/src/main/kotlin/{{PACKAGE_PATH}}/core/data/persistence/SampleEntity.ktpackage {{PACKAGE_ID}}.core.data.persistence
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "samples")
data class SampleEntity(
@PrimaryKey val id: String,
val label: String,
)
core/data/src/main/kotlin/{{PACKAGE_PATH}}/core/data/persistence/SampleDao.ktpackage {{PACKAGE_ID}}.core.data.persistence
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface SampleDao {
@Query("SELECT * FROM samples") fun observe(): Flow<List<SampleEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(entity: SampleEntity)
}
Enable schema export in core/data/build.gradle.kts:
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
core/data/src/main/kotlin/{{PACKAGE_PATH}}/core/data/datastore/AppPreferences.ktpackage {{PACKAGE_ID}}.core.data.datastore
import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val Context.prefs by preferencesDataStore(name = "app")
class AppPreferences(private val context: Context) {
private val onboardingDone = booleanPreferencesKey("onboarding_done")
fun onboardingDone(): Flow<Boolean> =
context.prefs.data.map { it[onboardingDone] ?: false }
suspend fun setOnboardingDone(done: Boolean) {
context.prefs.edit { it[onboardingDone] = done }
}
}
In {{APP_CLASS}}.onCreate (no direct Firebase API calls at this layer — keep behind an interface; just ensure init + the plugin):
override fun onCreate() {
super.onCreate()
// google-services plugin initializes Firebase automatically when a
// google-services.json is present for the current flavor.
// Crashlytics collection is controlled via manifest meta-data or runtime setCrashlyticsCollectionEnabled().
}
Per-flavor file layout reminder: drop google-services.json into app/src/dev/ and app/src/prod/. Never commit to app/ root; the plugin will then apply to every variant.
kapt. KSP only (Hilt 2.48+, Room 2.6+ all support KSP).@Serializable + kotlinx-serialization plugin.android. imports in :core:domain. It's a pure-JVM module for a reason — unit-testable without Robolectric.:app. Cross the boundary only via interfaces from :core:domain."2.1.0" in a module build.gradle.kts.Activity / ViewModel / Service that needs injection. Don't sprinkle EntryPoint unless you truly have a non-Hilt consumer.exportSchema = true is mandatory for Room once the app ships. The schemas/ directory goes into version control.Emit a block the command prints to the user:
Scaffold complete. Next steps:
☐ Add release signing config in app/build.gradle.kts:
signingConfigs {
create("release") {
storeFile = file(System.getenv("SIGNING_KEYSTORE") ?: "release.keystore")
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
}
}
☐ [if Firebase] drop google-services.json into app/src/dev/ and app/src/prod/.
☐ [if Firebase] confirm ./gradlew :app:processDevDebugGoogleServices runs without error.
☐ [if Room] confirm app/schemas/ is populated after ./gradlew :app:kspDevDebugKotlin.
☐ Replace the splash screen placeholder with the first real feature.
Build and run:
./gradlew :app:installDevDebug
./gradlew :app:testDebugUnitTest
npx claudepluginhub dimitriremoiville/cc-mobile --plugin cc-mobile-androidSearches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.