iru-setup-android-app
Invoked as /iru-setup-android-app with optional key: value args, usually by iru-setup-android-repository. Scaffolds a single-module Android application (Gradle Kotlin DSL, Compose or Views, signing, B
SKILL.md.claude/skills/iru-setup-android-app/SKILL.md
GitHubSetup Android App
Generate a complete, buildable Gradle Kotlin-DSL Android application project — wrapper, root build files, and a
single app/ com.android.application module — from explicit example templates embedded in this skill (Step 5)
that are genericized from https://github.com/albertoirurueta/irurueta-android-glutils, a real Android
repository whose Gradle toolchain (root build.gradle.kts, settings.gradle.kts, gradle.properties,
gradle/libs.versions.toml, gradle/wrapper/gradle-wrapper.properties) and app/ module this skill's templates
are drawn from line for line, with only project-identity fields turned into <placeholder> markers. This skill is
self-contained: it does not read or reference iru-setup-android-library's files at run time, even though a
companion iru-setup-android-library skill draws its own toolchain templates from the same reference repository —
if a repository ends up with both a lib/ and an app/ module, run each skill once and reconcile the two
generated settings.gradle.kts/root build.gradle.kts files by hand (Step 8 calls this out).
Exceptions to "genericized": com.irurueta:irurueta-android-test-utils (Gradle catalog alias irurueta-test-utils)
is kept as a real third-party Robolectric/MockK test-helper dependency, exactly as the reference repository uses
it — it is not a placeholder.
This skill produces only the app/ module. It never writes a mavenPublishing {} block, a sign/publish CI
profile, or anything that assumes the output is redistributed as a library artifact — release builds are meant to
be installed on a device or uploaded to a store, not published to Maven Central.
Step 0 — Resolve inputs
This skill can be invoked stand-alone (/iru-setup-android-app) or as a step inside another skill (e.g. a future
iru-setup-android-app-repository or the catalog's iru-setup-repository front door), which resolves these same
inputs itself and passes them through args as key: value lines, one per line, e.g.:
application-id: com.example.myapp
namespace: com.example.myapp
app-name: My App
min-sdk: 26
compile-sdk: 36
target-sdk: 36
version-name: 1.0.0
version-code: 1
license: Apache License 2.0
developer-name: Jane Doe
developer-email: jane@example.com
organization-url: https://github.com/example-org
open-source: yes
sonar: cloud
sonar-organization: example-org-github
sonar-project-key: example-org_my-app
sonar-host-url: https://sonarcloud.io
ui: compose
distribution: store
built-in-kotlin: no
junit: 4
coverage-tool: jacoco
static-analysis: lint
accessibility: enforce
visibility: public
ci: yes
mode: new
on-existing: ask
Parse any such lines from args now. Every field found here is resolved — skip asking about it in Step 2. Only
fields genuinely missing from args still need a question. If args is absent or doesn't look like this format,
treat everything as unset and ask normally.
Recognized keys: application-id, namespace (defaults to application-id if omitted), app-name, min-sdk
(default 26), compile-sdk/target-sdk (default: the current stable Android API level, looked up in Step 4),
version-name (default 1.0.0), version-code (default 1), license, developer-name, developer-email,
organization-url, open-source (yes/no), sonar (cloud/self-hosted/none) plus, only when sonar is
cloud or self-hosted, sonar-organization/sonar-project-key/sonar-host-url, ui (compose/views),
distribution (none/internal/store), built-in-kotlin (no/yes), junit (4/5), coverage-tool
(jacoco/kover), static-analysis (lint/lint+detekt+ktlint), accessibility (enforce/report/off/none,
default enforce — the automated accessibility level of the UI tests, written to .ui-quality.yml), visibility (public/private/internal —
GitHub repository visibility, only used to pick the sonar recommendation in Step 2), ci (yes/no — whether
CI workflows will exist to send Sonar reports; only used by Step 2's sonar question), mode (new/existing),
and on-existing (ask/keep/update/replace, default ask).
mode: existing is this catalog's shared signal (set by a front door that already ran iru-explore and knows this
is an established repository). It no longer means "gap-fill silently": Step 1 still warns about, and asks about,
every pre-existing file group. Only on-existing skips that question — keep (create only missing files, leave
every existing one untouched — the old mode: existing behaviour), update, or replace is then applied to every
pre-existing file group without asking (the warning is still printed). on-existing is meant for scripted runs; an
orchestrator passes it only if its own args received it.
Accessibility level none and assistive-technology templates. accessibility takes enforce, report, off or
none (UI contract §8; the stand-alone question gains a fourth option, None — opt out entirely, e.g. a mandated
non-compliant design). With none: write accessibility: none to .ui-quality.yml, do not install or configure the
accessibility test tooling (the Accessibility Test Framework (ATF) audit), and omit the accessibility-specific attributes from the generated templates.
With enforce, report or off: include the assistive-technology attributes from the contract's §8 platform
subsections in every generated template — contentDescription (or Modifier.semantics) on icon buttons and images, sp font sizes, 48dp touch targets, TalkBack-friendly traversal — and, for enforce/report only, the automated checks. In
mode: existing, a prior .ui-quality.yml follows this skill's existing Update / Keep / Replace prompt and its
accessibility value is never silently flipped (Update sets it only when args supplied it).
Templates. The file templates of Step 5 are files under this skill's templates/ directory, not inline text. Read only the template files for the ui variant resolved in Step 2 (templates/common/ for both variants, plus templates/compose/ or templates/views/; the index at the top of Step 5 says which applies); never read the other variant's files. The opt-in snippets (distribution, Detekt/Spotless, Kover, JUnit 5) stay inline in Step 5.
Step 1 — Survey the repository
Check which of Step 5's files already exist at the repository root — above all settings.gradle.kts and
app/build.gradle.kts, but also root build.gradle.kts, gradle.properties, gradle/libs.versions.toml, the
Gradle wrapper files, detekt.yml, .ui-quality.yml, and the app/ skeleton files (manifest, proguard-rules.pro, res/ files,
MainActivity.kt/theme, test sources, app/.gitignore). Then tell the user which of the two this run is doing:
Scaffolding from scratch — none exist: skip straight to Step 2. There is nothing to preserve.
Updating an existing Gradle project —
app/build.gradle.ktsexists and appliescom.android.application(this skill, or a hand-written equivalent, already owns this module), or any other of Step 5's files exists: warn the user, listing every file found by path, that each existing file group below will be decided individually (files that don't exist yet are still created from scratch alongside). This applies undermode: existingtoo.Group the existing files into these artifacts (only those with at least one file present):
settings.gradle.kts, rootbuild.gradle.kts,gradle/libs.versions.toml,gradle.properties, the Gradle wrapper,app/build.gradle.kts, theapp/skeleton (manifest,proguard-rules.pro,res/files,MainActivity.kt/theme or layout, test sources includingtesting/UiScreenshots.kt,app/.gitignore),.ui-quality.yml(a conflict to name: itsaccessibilitydiffers from this run's), anddetekt.yml(only once Step 2 resolvesstatic-analysis: lint+detekt+ktlint— ask about it after Step 2 in that case). Unlesson-existingdecides (Step 0), ask withAskUserQuestion, one question per artifact (batch up to 4 per call; further calls for the rest), naming any concrete conflict with this run's choices (e.g. "app/build.gradle.ktshas asonar {}block but you chosesonar: none", "it appliescom.google.firebase.appdistributionbutdistributionisstore"). Options, with the consequence in each description:- Update (Recommended for the build files and the wrapper) — "Merge: add what's missing and correct outdated
parts using this skill's template as reference — missing plugins/version-catalog aliases, the
sonar {}/signingConfigs/ distribution-plugin blocks added or removed per this run's choices, toolchain versions bumped to Step 4's lookup; keep every dependency, product flavor, build type and option this skill doesn't own. You review a diff; your customizations survive, but a customized part this skill owns may be rewritten." For theapp/skeleton, Update only creates its missing files. - Keep as is (Recommended for the
app/skeleton and.ui-quality.yml) — "Leave the file(s) untouched. Nothing you customized changes, but they may stay out of date or disagree with this run's choices — the report will list each mismatch." - Replace — "Regenerate from this skill's template. Result matches this house pattern exactly, but every
customization (dependencies, flavors, build types beyond Step 5's template) is lost — recoverable only from git
history." Never the default. Even on Replace, never overwrite
app/src/main/java/**application source beyond the placeholder files this skill created originally (detect by exact content match against Step 5's templates before overwriting any Kotlin file), and never touchAndroidManifest.xml's<application>children beyond the one<activity>this skill adds. - Stop (first question only) — "End this skill now without changing any file." Report this and end here.
Note in Step 8's report the action taken per artifact.
- Update (Recommended for the build files and the wrapper) — "Merge: add what's missing and correct outdated
parts using this skill's template as reference — missing plugins/version-catalog aliases, the
settings.gradle.ktsexists but has noappmodule applyingcom.android.application, orapp/build.gradle.ktsexists and appliescom.android.library: this is not this skill's scaffold (a library-only or differently laid out project). Warn the user explicitly and ask whether to add anappmodule alongside the existing one, or stop. Don't attempt to convert an existing module.
Step 2 — Collect the required inputs
For any field Step 0 already resolved from args, use that value directly and don't ask about it again. For
everything else, ask the user directly (plain conversation, pre-filling defaults found in Step 1 if updating an
existing scaffold):
- application-id — reverse-DNS application id, e.g.
com.example.myapp(written todefaultConfig.applicationIdand used to derive the Kotlin package path underapp/src/main/java/). - namespace — the module's Kotlin/resource namespace (
android.namespace); defaults toapplication-idif the user gives none — offer that as the default rather than asking open-endedly. (namespaceandapplicationIdare independent in modern AGP — a namespace change alone never changes what's installed on a device — but keeping them equal is the common case and this skill's default.) - app-name — the display name shown under the launcher icon (
strings.xml'sapp_name). - min-sdk — default
26(Android 8.0).26is also the API level adaptive launcher icons were introduced at, which is why Step 5's icon template needs no legacy raster fallback (see that step's note). - compile-sdk/target-sdk — default to the current stable Android API level, looked up in Step 4; offer that
looked-up value as the default rather than asking open-endedly.
target-sdkdefaults to the same value ascompile-sdkunless the user wants to target an older platform behavior deliberately. - version-name — default
1.0.0(Android apps don't use Maven's-SNAPSHOTconvention;iru-android-bump-versionis the skill that later rewrites this). - version-code — default
1(a plain monotonically increasing integer, not derived fromversion-name). - Developer name, developer email, organizationUrl — recorded in Step 8's report only; unlike
iru-setup-android-library, nopom/mavenPublishingblock exists here to put them in.
Then, unless Step 0 already resolved a license value from args, ask about the license with
AskUserQuestion (mirrors this catalog's other setup skills):
- MIT License (recommended for app source not meant for redistribution as a library)
- Apache License 2.0
- No license (proprietary / all rights reserved)
- Other — ask for the license's display name and URL directly afterward
If a license is chosen (anything but "No license"), remind the user in Step 8 to also add a matching LICENSE
file at the repository root if one doesn't exist yet — iru-check-license can verify/backfill source headers
against it once it's there.
Then, unless Step 0 already resolved them, ask about ui, built-in-kotlin, junit, coverage-tool,
static-analysis, and accessibility as bounded choices (AskUserQuestion, max four options each):
- ui —
compose(recommended default; Jetpack Compose Material 3, matches this catalog's other app-flavor skills likeiru-setup-react-native-app) orviews(classicAppCompatActivity+ XML layout + ViewBinding, no Compose runtime at all — pick this only when the user explicitly wants the older View system, e.g. to match an existing design-system investment). Gates which of Step 5'sMainActivity/layout templates get written, and whetherkotlin-compose/androidx-compose-bom/androidx-material3are in the version catalog at all. - built-in-kotlin —
no(default, matches the reference project'sandroid.builtInKotlin=false) oryes(opts into AGP's built-in Kotlin compilation support instead of the separateorg.jetbrains.kotlin.androidplugin — still experimental as of AGP 9.x; only recommendyesif the user explicitly wants to try it). - junit —
4(default; Robolectric + MockK + JUnit 4, exactly as the reference project) or5(adds thede.mannodermaus.android-junitplugin soapp/src/testcan also use JUnit 5 Jupiter alongside the JUnit-4-only Robolectric runner — see Step 5's opt-ins). - coverage-tool —
jacoco(default; AGP's built-increate<Variant>UnitTestCoverageReporttask, no extra plugin) orkover(addsorg.jetbrains.kotlinx.koverfor a Kotlin-native coverage report/verification DSL instead). - static-analysis —
lint(default; Android Lint only, already built into AGP) orlint+detekt+ktlint(adds Detekt for Kotlin static analysis and Spotless-driven ktlint formatting — see Step 5's opt-ins). - accessibility — "Support accessibility and assistive technologies?":
enforce(Recommended — Accessibility Test Framework violations fail the UI tests),report(listed, never failing), oroff(not run; the coding defaults still apply). Written to.ui-quality.yml(Step 5).
Then resolve open-source, sonar, and distribution — skip any of the three Step 0 already resolved from
args. When invoked stand-alone with none of them pre-resolved, ask open-source first (AskUserQuestion:
yes/no) and derive the recommended default for sonar from that answer, per this catalog's shared convention:
- open-source — is this repository open source? Yes / No.
- sonar — whether to wire up a SonarQube/SonarCloud scan (this house pattern runs it via the
org.sonarqubeGradle plugin, invoked as./gradlew :app:sonar— typically from the CI workflowiru-setup-android-github-workflowsgenerates, so it's worth setting up here even if that workflow comes later). Recommend SonarCloud (cloud) only when open-source is Yes andvisibilityispublic(or unset — this skill doesn't resolvevisibilityitself when invoked standalone; an unset value is treated as matchingopen-source); otherwise state that SonarCloud is free only for public/open-source projects (a private project needs a paid plan) and recommend None (none), offering self-hosted SonarQube (free Community Build, but you run the server) as the second option. Ifci: nowas supplied, say plainly that no CI workflow would send reports — analysis would only run when someone runs./gradlew :app:sonarmanually with a token — and recommendnone; if the user still pickscloud/self-hosted, write thesonar {}block only and note in Step 8 that no CI step sends reports:cloud— ask for thesonar.organizationkey, offering<owner>-github(the owner parsed in Step 3) as the suggested default. Defaultsonar.projectKeyto<owner>_<repo>andsonar.host.urltohttps://sonarcloud.io, confirming both with the user rather than assuming silently.self-hosted— ask forsonar.host.urldirectly (no sensible default), plussonar.organizationonly if that server has organizations enabled, andsonar.projectKey.none— skip the entiresonar { properties {…} } }block and theorg.sonarqubeplugin (Step 5/6).
- distribution —
none/internal/store:none— release builds are for local install/manual QA only. No distribution plugin is added;signingConfigsis still written (Step 5) so a signed release build remains possible whenever the signing secrets exist, but nothing uploads it anywhere.internal— adds the Firebase App Distribution Gradle plugin (com.google.firebase.appdistribution, version looked up in Step 4) toapp/build.gradle.kts, so./gradlew :app:appDistributionUploadReleasecan push a build straight to Firebase's tester groups. This needs a Firebase project:google-services.json(the Firebase config file, unique per Firebase project) is referenced but never written by this skill — it must be downloaded from the Firebase console and placed atapp/google-services.json, and Step 5 gitignores it (it contains project-specific API keys/identifiers that shouldn't be committed). Step 8 flags theFIREBASE_APP_IDand a Firebase service-account credential (JSON key, typically stored as a CI secret and referenced via--service-credentials-fileor theFIREBASE_TOKEN/GOOGLE_APPLICATION_CREDENTIALSenv var) that a CI-driven upload will need.store— full Google Play production submission is intended. Ask viaAskUserQuestionhow the upload should happen (bounded choice, two options):- Workflow-level action (recommended default) — leave
app/build.gradle.ktsalone; the actualr0adkll/upload-google-play@v1GitHub Actions step is generated later byiru-setup-android-github-workflowswhen it renders the release workflow fordistribution: store. This skill only records the choice (Step 8) so that skill doesn't have to ask again. - Gradle Play Publisher plugin — adds
com.github.triplet.play(com.github.triplet.playid, version looked up in Step 4) toapp/build.gradle.ktswith a minimalplay { serviceAccountCredentials.set(file(<path>)) }block (Step 5), so./gradlew :app:publishBundlecan upload directly from any machine/CI runner that has the service-account JSON, without depending on theupload-google-playAction at all. Either choice needs the same underlying Google Play service-account JSON key — this skill never collects or stores it, only names it in Step 8's report.
- Workflow-level action (recommended default) — leave
Step 3 — Infer repository information
Don't ask for these — derive them from the current repository, the same way iru-setup-java-library and
iru-setup-react-native-app do:
- Owner/repo and host: parse
git remote get-url origin(handles bothgit@host:owner/repo.gitandhttps://host/owner/repo.gitforms). Used for Step 2'ssonar.organization/sonar.projectKeydefaults andsettings.gradle.kts'srootProject.name. If there's nooriginremote yet, ask the user directly instead of leaving these blank.
Step 4 — Look up toolchain versions
Every version below must be looked up at run time; fall back to the value in this table (what this skill's author actually observed in September 2026) only when the lookup fails, and say so explicitly in Step 8's report.
| Component | Lookup | September 2026 fallback |
|---|---|---|
| Gradle | https://services.gradle.org/versions/current ("version"/"checksum" fields — "checksum" in that same JSON response is already the binary distribution's SHA-256, no second fetch needed; "checksumUrl" also works) |
9.7.1, sha256 acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a — live-confirmed during this skill's verification, and used as-is against AGP 9.0.1 below with no compatibility issue |
| Android Gradle Plugin (AGP) | https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/maven-metadata.xml (read the highest non--alpha/-beta/-rc <version> entry — this AGP artifact's own metadata, not AAPT2's, and not its <latest>/<release> tags, which pointed at an alpha during this verification) |
9.0.1 — the reference project's own verified-working version; a live lookup during this verification found 9.4.0 already stable, but the fallback here is deliberately the older, known-compatible-with-Kotlin-2.3.10 version actually built and tested for this skill (see the compatibility note below the table) |
| Kotlin | https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin/maven-metadata.xml — prefer this over search.maven.org's solrsearch API, confirmed stale below |
2.3.10 — likewise the version actually verified against AGP 9.0.1/Compose BOM 2026.02.01 together, not the bare current release |
| Dokka | https://repo1.maven.org/maven2/org/jetbrains/dokka/dokka-gradle-plugin/maven-metadata.xml — same staleness caveat as Kotlin above |
2.1.0 |
| SonarQube Gradle plugin | https://plugins.gradle.org/m2/org/sonarqube/org.sonarqube.gradle.plugin/maven-metadata.xml |
7.2.3.7755 |
| JUnit 4 | https://search.maven.org/solrsearch/select?q=g:junit+AND+a:junit&rows=1&wt=json |
4.13.2 |
AndroidX Test junit/junit-ktx |
https://dl.google.com/dl/android/maven2/androidx/test/ext/junit/group-index.xml |
1.3.0 |
AndroidX Espresso espresso-core |
https://dl.google.com/dl/android/maven2/androidx/test/espresso/espresso-core/group-index.xml |
3.7.0 |
AndroidX Compose BOM (only ui: compose) |
https://dl.google.com/dl/android/maven2/androidx/compose/compose-bom/group-index.xml |
2026.02.01 |
AndroidX activity-compose (only ui: compose; not covered by the Compose BOM, needs its own version) |
https://dl.google.com/dl/android/maven2/androidx/activity/group-index.xml |
1.13.0 |
com.google.android.material:material |
https://dl.google.com/dl/android/maven2/com/google/android/material/material/group-index.xml |
1.13.0 |
AndroidX Test core-ktx |
https://dl.google.com/dl/android/maven2/androidx/test/core/group-index.xml |
1.7.0 |
MockK (mockk-android) |
https://search.maven.org/solrsearch/select?q=g:io.mockk+AND+a:mockk-android&rows=1&wt=json |
1.14.9 |
| Robolectric | https://search.maven.org/solrsearch/select?q=g:org.robolectric+AND+a:robolectric&rows=1&wt=json |
4.16.1 |
kotlin-reflect |
same as the Kotlin row above (kept in lockstep) | 2.3.10 |
com.irurueta:irurueta-android-test-utils |
https://search.maven.org/solrsearch/select?q=g:com.irurueta+AND+a:irurueta-android-test-utils&rows=1&wt=json |
1.3.2 |
Current stable Android API level (compile-sdk/target-sdk default) |
sdkmanager --list on the local SDK, or https://apilevels.com |
36 (Android 16) |
Roborazzi (roborazzi, roborazzi-compose for ui: compose, roborazzi-accessibility-check; test scope only) |
https://repo1.maven.org/maven2/io/github/takahirom/roborazzi/roborazzi/maven-metadata.xml (<release>) |
1.76.0 |
Compose UI test (ui-test-junit4, ui-test-manifest, ui-test-junit4-accessibility; only ui: compose) |
versioned by the Compose BOM above — no own row; ui-test-junit4-accessibility needs Compose 1.8+ (BOM 2025.04+) |
— |
Espresso accessibility (espresso-accessibility; only ui: views) |
same version as espresso-core above (same androidx.test.espresso group-index) |
3.7.0 |
| Detekt (opt-in) | https://plugins.gradle.org/m2/io/gitlab/arturbosch/detekt/io.gitlab.arturbosch.detekt.gradle.plugin/maven-metadata.xml |
1.23.8 — unverified locally, confirm at run time |
| Spotless (opt-in) | https://plugins.gradle.org/m2/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/maven-metadata.xml |
7.2.1 — unverified locally, confirm at run time |
Kover (opt-in, coverage-tool: kover) |
https://plugins.gradle.org/m2/org/jetbrains/kotlinx/kover/org.jetbrains.kotlinx.kover.gradle.plugin/maven-metadata.xml |
0.9.2 — unverified locally, confirm at run time |
de.mannodermaus.android-junit (opt-in, junit: 5) |
https://plugins.gradle.org/m2/de/mannodermaus/android-junit/de.mannodermaus.android-junit.gradle.plugin/maven-metadata.xml <release> — the plugin id dropped its trailing 5 when the mannodermaus/android-junit5 project was renamed android-junit-framework; a lookup against the old de.mannodermaus.android-junit5 coordinate returns nothing (same finding as iru-setup-android-library) |
2.0.1 (the value iru-setup-android-library resolved) — unverified locally by this skill, confirm at run time |
Firebase App Distribution plugin (opt-in, distribution: internal) |
https://plugins.gradle.org/m2/com/google/firebase/appdistribution/com.google.firebase.appdistribution.gradle.plugin/maven-metadata.xml |
5.1.1 — unverified locally, confirm at run time |
Gradle Play Publisher plugin (opt-in, distribution: store + plugin choice) |
https://plugins.gradle.org/m2/com/github/triplet/play/com.github.triplet.play.gradle.plugin/maven-metadata.xml |
3.11.1 — unverified locally, confirm at run time |
Compatibility over bare "latest", confirmed the hard way while verifying this skill: looking up every artifact's
own individual latest version and combining them all is not the same as a combination someone has actually built.
This skill's fallbacks (AGP/Kotlin/Dokka/Compose BOM/etc.) are the reference project's own real, CI-verified
combination, not each package's independently-latest release — prefer that same discipline when a live lookup
succeeds too: resolve AGP/Kotlin/the Compose BOM together and sanity-check ./gradlew :app:assembleDebug once with
the new versions before trusting them, rather than bumping each in isolation. search.maven.org's solrsearch
API returned a stale latestVersion for both kotlin-gradle-plugin (2.2.0) and dokka-gradle-plugin (2.0.0)
during this verification, each already superseded by a newer stable release visible in Maven Central's own
maven-metadata.xml for the same artifact — treat solrsearch results as a hint, not ground truth, and prefer a
direct maven-metadata.xml/group-index fetch when precision matters (as it does for Kotlin/AGP compatibility).
Step 5 — Reference templates
Templates. Every template below is a file under templates/<variant>/<path of the generated file> (<package-path> appears as __package-path__ in directory names), holding the exact bytes of that generated file's template. Read only the files for the ui variant resolved in Step 2, each in full, before filling placeholders:
| Generated file | Template | Applies when |
|---|---|---|
gradle/wrapper/gradle-wrapper.properties |
templates/common/gradle/wrapper/gradle-wrapper.properties |
always |
build.gradle.kts |
templates/common/build.gradle.kts |
always |
settings.gradle.kts |
templates/common/settings.gradle.kts |
always |
gradle.properties |
templates/common/gradle.properties |
always |
gradle/libs.versions.toml ([versions] section) |
templates/common/gradle/libs.versions.toml.versions |
always |
gradle/libs.versions.toml ([libraries] section) |
templates/common/gradle/libs.versions.toml.libraries |
always |
gradle/libs.versions.toml ([plugins] section) |
templates/common/gradle/libs.versions.toml.plugins |
always |
app/build.gradle.kts |
templates/common/app/build.gradle.kts |
always (gated blocks inside it follow ui, sonar, distribution, coverage-tool) |
app/proguard-rules.pro |
templates/common/app/proguard-rules.pro |
always |
app/src/main/AndroidManifest.xml |
templates/common/app/src/main/AndroidManifest.xml |
always |
app/src/main/res/values/colors.xml |
templates/common/app/src/main/res/values/colors.xml |
always |
app/src/main/res/values/strings.xml |
templates/common/app/src/main/res/values/strings.xml |
always |
app/src/main/res/values/themes.xml (and, with darker colors, values-night/themes.xml) |
templates/views/app/src/main/res/values/themes.xml |
ui: views |
app/src/main/res/values/themes.xml |
templates/compose/app/src/main/res/values/themes.xml |
ui: compose |
app/src/main/java/<package-path>/ui/theme/Theme.kt |
templates/compose/app/src/main/java/__package-path__/ui/theme/Theme.kt |
ui: compose |
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml and ic_launcher_round.xml (same content) |
templates/common/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml |
both ui variants |
app/src/main/res/drawable/ic_launcher_background.xml |
templates/common/app/src/main/res/drawable/ic_launcher_background.xml |
both ui variants |
app/src/main/res/drawable/ic_launcher_foreground.xml |
templates/common/app/src/main/res/drawable/ic_launcher_foreground.xml |
both ui variants |
app/src/main/java/<package-path>/MainActivity.kt |
templates/compose/app/src/main/java/__package-path__/MainActivity.kt |
ui: compose (default) |
app/src/main/res/layout/activity_main.xml |
templates/views/app/src/main/res/layout/activity_main.xml |
ui: views |
app/src/main/java/<package-path>/MainActivity.kt |
templates/views/app/src/main/java/__package-path__/MainActivity.kt |
ui: views |
app/src/test/resources/<package-path>/robolectric.properties |
templates/common/app/src/test/resources/__package-path__/robolectric.properties |
always |
app/src/test/java/<package-path>/MainActivityTest.kt |
templates/common/app/src/test/java/__package-path__/MainActivityTest.kt |
always |
app/src/androidTest/java/<package-path>/MainActivityInstrumentedTest.kt |
templates/compose/app/src/androidTest/java/__package-path__/MainActivityInstrumentedTest.kt |
ui: compose |
app/src/androidTest/java/<package-path>/MainActivityInstrumentedTest.kt |
templates/views/app/src/androidTest/java/__package-path__/MainActivityInstrumentedTest.kt |
ui: views |
.ui-quality.yml |
templates/common/.ui-quality.yml |
always |
app/src/test/java/<package-path>/testing/UiScreenshots.kt |
templates/common/app/src/test/java/__package-path__/testing/UiScreenshots.kt |
both ui variants |
app/src/test/java/<package-path>/GreetingTest.kt |
templates/compose/app/src/test/java/__package-path__/GreetingTest.kt |
ui: compose |
app/src/test/java/<package-path>/MainActivityScreenTest.kt |
templates/views/app/src/test/java/__package-path__/MainActivityScreenTest.kt |
ui: views |
app/.gitignore |
templates/common/app/.gitignore |
always |
These templates are genericized from https://github.com/albertoirurueta/irurueta-android-glutils's actual
toolchain files and app/ module — kept as close to verbatim as possible, with only project-identity fields turned
into <placeholder> markers. Substitute using Steps 2–4; keep everything else exactly as shown unless a step below
says an opt-in changes it.
<placeholder> resolution table
| Placeholder | Source |
|---|---|
<project-name> |
settings.gradle.kts's rootProject.name — the repository name from Step 3 |
<application-id> |
Step 2 |
<namespace> |
Step 2 |
<app-name> |
Step 2 |
<min-sdk> / <compile-sdk> / <target-sdk> |
Step 2/4 |
<version-name> / <version-code> |
Step 2 |
<gradle-version> / <gradle-distribution-sha256> |
Step 4 |
<agp-version> / <kotlin-version> / <dokka-version> / <sonarqube-plugin-version> / … |
Step 4 |
<sonar-organization> / <sonar-project-key> / <sonar-host-url> |
Step 2 |
<owner> / <repo> |
Step 3 |
<accessibility> |
Step 2 (enforce by default) — only in .ui-quality.yml |
<roborazzi-version> |
Step 4 |
Gradle wrapper — gradle/wrapper/gradle-wrapper.properties
Template: templates/common/gradle/wrapper/gradle-wrapper.properties — read it in full before filling placeholders
The reference project's own gradle-wrapper.properties predates distributionSha256Sum being set (it has none) —
this skill adds it anyway, since Gradle has supported wrapper checksum verification for years and it's a cheap
supply-chain hardening with no downside. gradlew/gradlew.bat/gradle/wrapper/gradle-wrapper.jar themselves are
binaries/scripts unfit for a Markdown template — obtain them one of two ways (Step 6 says which to try first):
- If a local Gradle install exists (
gradle -vsucceeds), rungradle wrapper --gradle-version <gradle-version>at the project root, then overwrite the generatedgradle-wrapper.propertieswith the block above (it adds thedistributionSha256Sumlinegradle wrapperalone doesn't add without--gradle-distribution-sha256-sum). - Otherwise, fetch the three files directly from the Gradle GitHub repository's matching tag:
https://raw.githubusercontent.com/gradle/gradle/v<gradle-version>/gradlew,.../gradlew.bat, and.../gradle/wrapper/gradle-wrapper.jar(binary — fetch withcurl -fsSL -o, not a text read), thenchmod +x gradlew.
Root build.gradle.kts
Template: templates/common/build.gradle.kts — read it in full before filling placeholders
The trailing project.delete { … } block is copied verbatim from the reference project — it runs at Gradle
configuration time (not as a clean task), deleting the root project's own (normally-empty) build directory on
every invocation. Harmless and intentional in the source repository; keep it for fidelity, but call it out in Step
8 since it surprises anyone expecting build.gradle.kts to only declare plugins.
settings.gradle.kts
Template: templates/common/settings.gradle.kts — read it in full before filling placeholders
Departure from the reference repository: the reference lists https://oss.sonatype.org/content/repositories/snapshots
as its snapshots repository, but OSSRH (and that host with it) was decommissioned in mid-2025 — -SNAPSHOT
artifacts published through the Central Portal now land in
https://central.sonatype.com/repository/maven-snapshots/, which is what this template uses so an app can still
consume a -SNAPSHOT build of a library published by iru-setup-android-library's develop.yml. Don't copy the
old URL back from the reference.
FAIL_ON_PROJECT_REPOS matches the reference project: it forbids any module-level build.gradle.kts from
declaring its own repositories {} block, forcing every dependency to resolve through this single
dependencyResolutionManagement block instead — Step 5's app/build.gradle.kts template relies on this and never
declares repositories itself.
gradle.properties
Template: templates/common/gradle.properties — read it in full before filling placeholders
<built-in-kotlin-flag> is false unless Step 2's built-in-kotlin answer was yes, in which case both
lines become true — the same coupling iru-setup-android-library's gradle.properties template uses, since
AGP's built-in Kotlin support is what the new DSL is paired with (the reference project opts out of both together,
false/false). Copied verbatim from the reference project's gradle.properties otherwise (no real
repository/org names appear in this file, so nothing else needed genericizing).
gradle/libs.versions.toml
Template: templates/common/gradle/libs.versions.toml.versions — read it in full before filling placeholders
Template: templates/common/gradle/libs.versions.toml.libraries — read it in full before filling placeholders
Both of the last two are required, not optional — confirmed while verifying this skill: the Compose BOM
(platform(libs.androidx.compose.bom)) only pins versions; it doesn't pull either artifact in transitively via
androidx.compose.material3:material3. Omitting androidx.activity:activity-compose fails
MainActivity.kt's setContent { } call with Unresolved reference 'setContent'; omitting
androidx.compose.ui:ui-tooling-preview fails the @Preview composable with Unresolved reference 'tooling' /
Unresolved reference 'Preview'. androidx.activity:activity-compose isn't part of the androidx.compose BOM
group at all, so it needs its own version (Step 4) even though ui-tooling-preview is covered by the BOM's
version pin once the artifact itself is declared.
Template: templates/common/gradle/libs.versions.toml.plugins — read it in full before filling placeholders
Aliases match the reference project's naming exactly (android-application, kotlin-android, kotlin-compose,
dokka, sonarqube, plus the library dependency names) so a repository that later also runs
iru-setup-android-library gets a version catalog with compatible alias spelling if the two files are merged by
hand. No android-library or publish alias/plugin is written here — this skill never scaffolds a library module
or a publishing block. Step 5's opt-ins (Detekt/Spotless/Kover/JUnit 5) each add their own [versions]/[plugins]
entries — see the "Opt-ins" subsection below.
app/build.gradle.kts
Template: templates/common/app/build.gradle.kts — read it in full before filling placeholders
Departures from the reference repository inside the sonar {} block (mirroring iru-setup-android-library's
templates — don't copy the reference's originals back):
sonar.coverage.jacoco.xmlReportPathslistsbuild/reports/coverage/test/debug/report.xml, not the reference'sbuild/reports/coverage/test/report.xml— the latter is never written. AGP'screateDebugUnitTestCoverageReportemits the XML under the variant-suffixedtest/debug/directory (see the verification notes after Step 8), which is also the pathiru-setup-android-github-workflows' workflows produce before running:app:sonar; the reference's path would silently report 0 % coverage on every push.sonar.junit.reportPathsandsonar.androidLint.reportPathsreplace the reference'ssonar.junit.reportsPathandsonar.android.lint.report— those two are not keys the SonarQube/SonarCloud analyzer reads (unknown properties are ignored), so with the reference's spelling test results and Android Lint findings are silently never imported.
signingConfigs safety, spelled out: hasReleaseSigning is computed once, from four env vars, before android {} runs. signingConfigs { if (hasReleaseSigning) { create("release") { … } } } means the "release"
SigningConfig object simply doesn't exist unless all four are present — and buildTypes { release { if (hasReleaseSigning) { signingConfig = signingConfigs.getByName("release") } } } only looks it up in that same
case, so getByName never throws UnknownDomainObjectException. The net effect: ./gradlew :app:assembleDebug
never needs signing secrets at all (debug builds use Android's auto-generated debug keystore, untouched by this
config); ./gradlew :app:assembleRelease/:app:bundleRelease succeed with no env vars set too, producing an
unsigned release APK/AAB (Gradle only warns, it doesn't fail) — confirmed in Step 7's verification. Set all
four (SIGNING_STORE_FILE as an absolute or project-relative path to a .jks/.keystore file,
SIGNING_STORE_PASSWORD, SIGNING_KEY_ALIAS, SIGNING_KEY_PASSWORD) to get a signed release build — this
skill never generates or stores a keystore itself (Step 8 repeats this).
bundleRelease/assembleRelease guidance:
./gradlew :app:assembleRelease— release APK, atapp/build/outputs/apk/release/. Fine for direct install/ Firebase App Distribution; not what Google Play accepts for new app submissions../gradlew :app:bundleRelease— release Android App Bundle (.aab), atapp/build/outputs/bundle/release/. This is what Google Play Console (and both ther0adkll/upload-google-play@v1Action and the Gradle Play Publisher plugin'spublishBundletask) expect.
distribution: internal addition — insert into the plugins {} block:
alias(libs.plugins.firebase.appdistribution)
and add to [versions]/[plugins] in gradle/libs.versions.toml:
firebaseAppdistribution = "<firebase-appdistribution-plugin-version>"
# [plugins]
firebase-appdistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppdistribution" }
No further Gradle DSL block is required for a minimal setup — the plugin reads app/google-services.json (never
written by this skill; see Step 2) and defaults appDistributionUploadRelease off the release build type. Add a
firebaseAppDistribution { releaseNotesFile = "..." ; groups = "..." } block later by hand if release notes/tester
groups should be fixed rather than passed via -P properties.
distribution: store + Gradle Play Publisher plugin chosen — insert into plugins {}:
alias(libs.plugins.triplet.play)
and add to gradle/libs.versions.toml:
tripletPlay = "<gradle-play-publisher-plugin-version>"
# [plugins]
triplet-play = { id = "com.github.triplet.play", version.ref = "tripletPlay" }
then, after the dependencies {} block:
play {
serviceAccountCredentials.set(file("<path-to-service-account-json>"))
defaultToAppBundles.set(true)
}
<path-to-service-account-json> is never a path this skill resolves or downloads — leave it as a placeholder
comment pointing at wherever CI decodes the GOOGLE_PLAY_SERVICE_ACCOUNT_JSON secret to (Step 8 flags this). If
the user instead chose the workflow-level action, skip this whole addition — app/build.gradle.kts stays exactly
as the base template above.
app/proguard-rules.pro
Template: templates/common/app/proguard-rules.pro — read it in full before filling placeholders
app/src/main/AndroidManifest.xml
Template: templates/common/app/src/main/AndroidManifest.xml — read it in full before filling placeholders
No package="<application-id>" attribute on <manifest>, deliberately — confirmed while verifying this skill:
current AGP no longer reads the application id/namespace from the manifest's package attribute at all; leaving
one in (as the reference project's manifest still does, predating this AGP behavior) produces a build-time warning
("Setting the namespace via the package attribute in the source AndroidManifest.xml is no longer supported, and the
value is ignored") for every build. android.namespace/defaultConfig.applicationId in app/build.gradle.kts
above are the only things that matter now — drop the attribute entirely rather than carry a dead, warning-generating
line forward from the reference.
android:theme="@style/Theme.App" is a placeholder style name kept short and generic (the reference project's own
manifest points at a theme name derived from its own repository name) — Step 5's theme templates below define
exactly Theme.App so the two always match; rename both together if the user wants a different style name.
app/src/main/res/** — theme, colors, strings, launcher icon
values/colors.xml:
Template: templates/common/app/src/main/res/values/colors.xml — read it in full before filling placeholders
values/strings.xml:
Template: templates/common/app/src/main/res/values/strings.xml — read it in full before filling placeholders
ui: views — values/themes.xml:
Template: templates/views/app/src/main/res/values/themes.xml — read it in full before filling placeholders
(and the same block, with darker color values swapped in, under values-night/themes.xml — mirrors the reference
project's day/night pair exactly.)
ui: compose — no XML theme is required; the manifest's android:theme="@style/Theme.App" still needs some
style to resolve at inflation time before Compose takes over, so write a minimal one:
Template: templates/compose/app/src/main/res/values/themes.xml — read it in full before filling placeholders
(no values-night variant needed — DayNight already switches automatically) and a Kotlin Compose theme wrapper,
app/src/main/java/<package-path>/ui/theme/Theme.kt:
Template: templates/compose/app/src/main/java/__package-path__/ui/theme/Theme.kt — read it in full before filling placeholders
Launcher icon (both ui variants) — mipmap-anydpi-v26/ic_launcher.xml and ic_launcher_round.xml:
Template: templates/common/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml — read it in full before filling placeholders
drawable/ic_launcher_background.xml (flat brand-color fill):
Template: templates/common/app/src/main/res/drawable/ic_launcher_background.xml — read it in full before filling placeholders
drawable/ic_launcher_foreground.xml (a simple centered monogram placeholder — replace with real brand art):
Template: templates/common/app/src/main/res/drawable/ic_launcher_foreground.xml — read it in full before filling placeholders
No raster (.webp/.png) launcher icons are generated, and none are needed. min-sdk defaults to 26 — the
exact API level <adaptive-icon> was introduced at — so every device this app installs on resolves the vector
adaptive icon directly; legacy mipmap-hdpi/mdpi/xhdpi/xxhdpi/xxxhdpi raster fallbacks (present in the reference
project only because it targets pre-26 launchers in some configurations) are dead weight here. If a user later
lowers min-sdk below 26, generate real raster fallbacks with Android Studio's Image Asset Studio at that point —
this skill deliberately doesn't attempt to rasterize vector art itself.
app/src/main/java/<package-path>/MainActivity.kt
ui: compose (default):
Template: templates/compose/app/src/main/java/__package-path__/MainActivity.kt — read it in full before filling placeholders
ui: views (ViewBinding-based alternative — write this instead when Step 2 chose views):
res/layout/activity_main.xml:
Template: templates/views/app/src/main/res/layout/activity_main.xml — read it in full before filling placeholders
MainActivity.kt:
Template: templates/views/app/src/main/java/__package-path__/MainActivity.kt — read it in full before filling placeholders
The constraints use Start/End, never Left/Right, so the layout mirrors in right-to-left locales (UI
contract §4). viewBinding = true (already in the buildFeatures {} block above) generates ActivityMainBinding from
activity_main.xml's filename — no manual binding class to write. This variant needs
androidx.appcompat:appcompat and androidx.constraintlayout:constraintlayout, both pulled in transitively by
com.google.android.material:material's own dependency graph, confirmed working with just the material alias
already in dependencies {} above (no extra explicit dependency line needed) — if that ever stops being true
upstream, add androidx.appcompat:appcompat and androidx.constraintlayout:constraintlayout explicitly.
app/src/test/resources/<package-path>/robolectric.properties
Template: templates/common/app/src/test/resources/__package-path__/robolectric.properties — read it in full before filling placeholders
The reference project pins this one minor step behind its own compileSdk (compileSdk = 36 → sdk=35) because
Robolectric's shipped Android SDK jars lag the very latest platform release by a few months after each Android
release — always check the installed robolectric version's supported SDKs before trusting <compile-sdk>
directly here (Robolectric errors clearly, naming which SDKs it has, if an unsupported one is requested); Step 7
records which value actually worked.
app/src/test/java/<package-path>/MainActivityTest.kt — placeholder unit test
Template: templates/common/app/src/test/java/__package-path__/MainActivityTest.kt — read it in full before filling placeholders
A genuine first test exercises MainActivity's onCreate via Robolectric.buildActivity(MainActivity::class.java)
— this skill ships only the arithmetic placeholder above (mirroring the reference project's own
ExampleUnitTest.kt) so iru-android-generate-all-tests has an obvious first target to replace.
app/src/androidTest/java/<package-path>/MainActivityInstrumentedTest.kt — instrumented test skeleton
ui: compose:
Template: templates/compose/app/src/androidTest/java/__package-path__/MainActivityInstrumentedTest.kt — read it in full before filling placeholders
ui: views — the same useAppContext test plus Espresso's Accessibility Test Framework integration:
Template: templates/views/app/src/androidTest/java/__package-path__/MainActivityInstrumentedTest.kt — read it in full before filling placeholders
Runs only via ./gradlew :app:connectedAndroidTest against a running emulator/device — Step 7 doesn't run this
(no emulator available in this skill's own verification environment); it was compile-checked
(:app:compileDebugAndroidTestKotlin) for both ui variants.
.ui-quality.yml — UI test settings at the repository root
Template: templates/common/.ui-quality.yml — read it in full before filling placeholders
<accessibility> is Step 2's answer (default enforce). Exactly this shape — the catalog's UI contract §8 — read
by UiScreenshots.kt below, iru-android-test's ui run, and the coding skills.
app/src/test/java/<package-path>/testing/UiScreenshots.kt — shared UI-test helper
Resolves UI_SCREENSHOTS_DIR (else <repository root>/.ui-screenshots), reads .ui-quality.yml, builds the
capture matrix and the contract's file names, forces Roborazzi to record (its captures are no-ops otherwise), and
maps the accessibility level onto Roborazzi's ATF options. Identical for both ui variants:
Template: templates/common/app/src/test/java/__package-path__/testing/UiScreenshots.kt — read it in full before filling placeholders
app/src/test/java/<package-path>/GreetingTest.kt (ui: compose) — sample UI test
Template: templates/compose/app/src/test/java/__package-path__/GreetingTest.kt — read it in full before filling placeholders
With a Compose BOM whose UI test artifacts are 1.11 or newer, createComposeRule is deprecated in favour of
androidx.compose.ui.test.junit4.v2.createComposeRule (same use) — switch the import then; the fallback BOM in
Step 4 predates it.
app/src/test/java/<package-path>/MainActivityScreenTest.kt (ui: views) — sample UI test
Template: templates/views/app/src/test/java/__package-path__/MainActivityScreenTest.kt — read it in full before filling placeholders
app/.gitignore
Template: templates/common/app/.gitignore — read it in full before filling placeholders
local.properties guidance
Never write local.properties (it's machine-specific and always gitignored at the root — see
iru-setup-android-gitignore). Tell the user in Step 8 instead: on first checkout, either let Android Studio
generate it automatically, or create it by hand with one line, sdk.dir=<path-to-android-sdk> (e.g.
sdk.dir=/Users/<you>/Library/Android/sdk on macOS, sdk.dir=/home/<you>/Android/Sdk on Linux) — the SDK
components this scaffold needs (the <compile-sdk> platform, matching build-tools) are downloaded automatically by
AGP on first build as long as android.builder.sdkDownload isn't disabled and the SDK licenses are already
accepted (sdkmanager --licenses).
Opt-ins
Detekt + Spotless (static-analysis: lint+detekt+ktlint) — add to [versions]/[plugins]:
detekt = "<detekt-version>"
spotless = "<spotless-version>"
# [plugins]
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
apply both in app/build.gradle.kts's plugins {} and configure:
detekt {
config.setFrom(files("$rootDir/detekt.yml"))
buildUponDefaultConfig = true
}
spotless {
kotlin {
target("src/**/*.kt")
ktlint()
licenseHeaderFile(rootProject.file("spotless/license-header.kt"))
}
kotlinGradle {
target("*.gradle.kts")
ktlint()
}
}
Write a minimal detekt.yml at the repository root (build:\n maxIssues: 0\ncomplexity:\n active: true, or
generate a fuller baseline with ./gradlew detektGenerateConfig); Detekt's SARIF output
(app/build/reports/detekt/detekt.sarif) is what iru-android-code-quality/iru-setup-android-github-workflows's
CodeQL-adjacent SARIF upload step consumes. Add spotless/license-header.kt only if a license was chosen in Step 2
(the file's content is that license's standard header comment).
Kover (coverage-tool: kover, instead of the default AGP JaCoCo path) — add to [versions]/[plugins]:
kover = "<kover-version>"
# [plugins]
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
apply in app/build.gradle.kts's plugins {}; no further DSL is required for a minimal setup — Kover's default
koverXmlReportDebug/koverHtmlReportDebug tasks work against the debug build type out of the box. When Kover is
chosen, drop enableUnitTestCoverage = true from the debug {} block above (Kover manages its own coverage
instrumentation and the two mechanisms shouldn't run simultaneously) and use iru-android-coverage's Kover-XML
parsing path instead of the JaCoCo one.
JUnit 5 (junit: 5, additive — Robolectric's runner itself stays JUnit 4) — add to [versions]/[plugins]:
mannodermausAndroidJunit = "<mannodermaus-plugin-version>"
# [plugins]
mannodermaus-android-junit = { id = "de.mannodermaus.android-junit", version.ref = "mannodermausAndroidJunit" }
apply in app/build.gradle.kts's plugins {} as alias(libs.plugins.mannodermaus.android.junit) (the same alias
iru-setup-android-library uses), and add the plugin's own BOM-pinned JUnit 5 test dependencies per
its README (testImplementation(libs.junit.jupiter), testRuntimeOnly(libs.junit.jupiter.engine) — exact
coordinates come from that plugin's own version catalog recommendation at the version resolved in Step 4, since it
pins compatible JUnit 5 Jupiter/Platform versions together). Robolectric's @RunWith(RobolectricTestRunner::class)
tests keep using JUnit 4 exactly as before; JUnit 5 is for new tests that don't need Robolectric, written with
@Test/org.junit.jupiter.api.Test instead.
Step 6 — Scaffold the project
Apply Step 1's per-artifact choice to every file this step would write that already exists: Keep as is —
skip it; Update — merge the filled-in template into it after showing the user the diff (for the app/
skeleton, only its missing files are created); Replace — overwrite it, within Step 1's application-source
limits. Files that didn't exist are always written.
- Obtain the Gradle wrapper (Step 5's two-path recipe): try
gradle wrapper --gradle-version <gradle-version>first, falling back to fetchinggradlew/gradlew.bat/gradle/wrapper/gradle-wrapper.jarfrom the Gradle GitHub tag. Then hand-writegradle-wrapper.propertieswith Step 5's exact content (addingdistributionSha256Sum, which neither path adds by default without an extra flag).chmod +x gradlew. - Write the root
build.gradle.kts,settings.gradle.kts,gradle.properties, andgradle/libs.versions.tomlfrom Step 5, with Step 2/3/4's values substituted and theui/sonar/distribution/opt-in conditionals applied. - Create
app/src/main/java/<package-path>/(from<namespace>, dots turned into path separators) and writeapp/build.gradle.kts,app/proguard-rules.pro,app/src/main/AndroidManifest.xml, theres/theme/color/ string/icon files,MainActivity.kt(+ui/theme/Theme.ktforui: compose, orres/layout/activity_main.xmlforui: views),app/src/test/resources/<package-path>/robolectric.properties,app/src/test/java/<package-path>/MainActivityTest.kt,app/src/test/java/<package-path>/testing/UiScreenshots.kt, the sample UI test (GreetingTest.ktforui: compose,MainActivityScreenTest.ktforui: views),app/src/androidTest/java/<package-path>/MainActivityInstrumentedTest.kt,app/.gitignore, and.ui-quality.ymlat the repository root (with Step 2'saccessibility) — all from Step 5. - Apply whichever opt-in blocks Step 2 selected (Detekt/Spotless, Kover, JUnit 5, Firebase App Distribution, Gradle Play Publisher) to the files just written, per Step 5's "Opt-ins"/distribution subsections.
- If
distribution: internalwas chosen, remind the user now (also repeated in Step 8) thatapp/google-services.jsonmust be downloaded from the Firebase console and placed manually — this skill never fetches or writes it.
Step 7 — Verify
Run through the iru-gate-runner agent (never inline in this skill's own context — its reports can be long):
./gradlew --offline help— confirms the wrapper, root/settings files, and version catalog all parse and resolve without touching the network (anything already cached from Step 6's own dependency resolution). If this fails offline only because a dependency genuinely isn't cached yet, retry without--offline../gradlew :app:assembleDebug— the real scaffold-sanity build. Warn the user up front that the first run downloads the<compile-sdk>platform, matching build-tools, and every Gradle/AGP/Kotlin/AndroidX artifact above if they aren't already cached — this can take several minutes and needsANDROID_HOME/local.propertiespointing at a real SDK install with licenses accepted../gradlew :app:testDebugUnitTest— runs the placeholder and the sample UI test; then confirm.ui-screenshots/component/Greeting/(ui: compose) or.ui-screenshots/screen/MainActivity/(ui: views) holds onedefault--<ltr|rtl>--<light|dark>.pngper.ui-quality.ymlcell (four by default).
Report pass/fail and any error output verbatim for a failure; don't dump a successful build's full log.
Step 8 — Report
Summarize what was generated: the resolved application-id/namespace/app-name, min-sdk/compile-sdk/
target-sdk, version-name/version-code, the license chosen (or "none"), and whether this run scaffolded from
scratch, updated an existing project (per Step 1), or the user chose to stop.
State explicitly what was included versus omitted, and why:
ui:compose/views— whichMainActivity/theme template was written.open-source/sonar: as Step 2 resolved them; ifsonar≠none, restate thesonar.organization/sonar.projectKey/sonar.host.urlwritten intoapp/build.gradle.kts'ssonar { properties {…} } }block (and, withci: no, that no CI step sends reports); ifnone, whether that's the expected default (not open source, a private/internalvisibility, orci: no) or a direct user choice.distribution:none/internal/store—internal: restate thatapp/google-services.jsonmust be downloaded from the Firebase console and placed at that exact path before any Firebase-related task runs, and thatFIREBASE_APP_IDplus a service-account credential will be needed by CI.store: restate which upload path was chosen (workflow-levelr0adkll/upload-google-play@v1, or the Gradle Play Publisher plugin) and the Google Play service-account JSON key it depends on either way — this skill never collects or stores it.none: no distribution plugin was added;signingConfigsis still present and usable whenever the fourSIGNING_*env vars are supplied later.
built-in-kotlin/junit/coverage-tool/static-analysis: as resolved, and which opt-in Gradle plugins/files that triggered.accessibilityand the UI-contract files:.ui-quality.ymlwith its action (created / updated / replaced / kept as is) and level, thetesting/UiScreenshots.kthelper, the sample UI test, and how many captures Step 7 found under.ui-screenshots/(gitignored byiru-setup-android-gitignore).- Which Step 4 versions came from a live lookup versus this skill's recorded fallback — call out explicitly any row marked "unverified locally" above that was used as-is without confirming it resolves.
Then report, per Step 1 artifact, the action actually taken — created / updated / replaced / kept as is — and
whether it came from the user's answer or on-existing; for each artifact kept as is, list any mismatch with this
run's choices. If existing toolchain files were replaced, explicitly list what could have been lost — any dependency,
flavor, or build-type customization beyond what Step 5's templates show — and tell the user to check git diff for
anything they need to re-add.
Known quirks / verification notes
Verified in
$TMPDIR/iru-verify/android/app(outside this repository):./gradlew :app:assembleDebug :app:testDebugUnitTest :app:lintall pass in one invocation against the scaffold generated withdistribution: store(workflow-level upload — no Gradle Play Publisher plugin applied),sonar: cloud(dummy org/key/host — thesonartask itself was never run, only thatapp/build.gradle.ktsconfigures with the block present),ui: compose,junit: 4,coverage-tool: jacoco,static-analysis: lint. Versions actually used: Gradle9.7.1(fresh, fromservices.gradle.org/versions/current— newer than the9.4.0recorded as this file's fallback; sha256 looked up from the same response), AGP9.0.1, Kotlin2.3.10, Dokka2.1.0, SonarQube plugin7.2.3.7755, JUnit4.13.2, AndroidXjunit1.3.0, Espresso3.7.0, Compose BOM2026.02.01,material1.13.0,test-core-ktx1.7.0, MockK1.14.9, Robolectric4.16.1,kotlin-reflect2.3.10,irurueta-android-test-utils1.3.2(the reference project's own verified-working combination — deliberately used as-is rather than every individual artifact's own bare "latest", since AGP/Kotlin/Compose-compiler version skew across a whole toolchain is the actual compatibility risk, not any single artifact being one minor behind).search.maven.org'ssolrsearchAPI returned stalelatestVersionvalues for bothkotlin-gradle-plugin(2.2.0) anddokka-gradle-plugin(2.0.0) during this verification — both were already superseded (Kotlin2.4.20, confirmed viarepo1.maven.org's ownmaven-metadata.xml, is real; Dokka likely similarly stale). Prefer a directmaven-metadata.xml/group-index fetch oversolrsearchfor "current version" lookups; treatsolrsearch'slatestVersionfield as unreliable../gradlew :app:assembleReleasewith none of the fourSIGNING_*env vars set was also run (separately, withunset SIGNING_STORE_FILE SIGNING_STORE_PASSWORD SIGNING_KEY_ALIAS SIGNING_KEY_PASSWORDfirst) and confirmed to succeed (BUILD SUCCESSFUL), producingtest-app-1.0.0-release-unsigned.apkatapp/build/outputs/apk/release/— proves thesigningConfigs/buildTypes.releaseconditional guard above never fails configuration when signing secrets are absent, exactly as designed.The default JDK on this machine (JDK 26) is not used —
JAVA_HOMEwas pinned explicitly to JBR 21 (export JAVA_HOME=$(/usr/libexec/java_home -v 21)) for every./gradlewinvocation; not re-tested against JDK 17 in this pass (iru-setup-android-library's verification exercises that fallback), but the same JBR distribution is present locally at-v 17too. Record JDK 21 (or 17) as a hard prerequisite, not just a suggestion — this AGP/ Gradle combination was never tried against JDK 26.app/build/reports/lint-results-debug.xml(17 issues in this run, allWarning-severity "newer version available"/GradleDependency/NewerVersionAvailable/AndroidGradlePluginVersion— zero real correctness/ security findings, expected since Step 4's fallback versions are all one or more minor releases behind by design) and the matching.html/.txtsiblings are the Lint report paths (iru-android-code-qualityreads the XML).testDebugUnitTestresults land atapp/build/test-results/testDebugUnitTest/*.xml(JUnit XML — 1 test, 0 failures, confirmed —iru-android-testparses these) plus an HTML report atapp/build/reports/tests/testDebugUnitTest/. The debug APK lands atapp/build/outputs/apk/debug/test-app-1.0.0-debug.apk(base.archivesName's<project-name>-<versionName>[-<buildNumber>]pattern, confirmed).Coverage, confirmed working — no vendored JaCoCo CLI needed for the
appmodule in this AGP version:./gradlew :app:createDebugUnitTestCoverageReportruns (app:createDebugUnitTestCoverageReportis exposed for acom.android.applicationmodule exactly the same way it is for a library) and emits a real XML report atapp/build/reports/coverage/test/debug/report.xml(standard JaCoCo-format XML — package/class/method-level<counter type="..." missed="..." covered="...">). The raw.execfile does not land atapp/build/jacoco/testDebugUnitTest.exec(that's where the reference project's older-AGP/CI script expects it for its vendored-jar fallback) — in this verified AGP9.0.1, it's atapp/build/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.execinstead. Since AGP already emits the XML directly, this skill never needsiru-setup-android-library's vendoredjacococli.jarfallback path at all for theappmodule —iru-android-coverageshould readreport.xmlfrom the path above directly.The Firebase App Distribution and Gradle Play Publisher plugin ids/versions were checked only by resolving
./gradlew helpwith each plugin applied at its Step 4 fallback version (confirms the plugin id + version exist and configure without error) — no distribution task was ever run (appDistributionUploadRelease,publishBundle), since both need real Firebase/Google Play credentials this environment doesn't have. Mark both as "unverified locally" beyond that configuration-time check whenever a live plugin-portal lookup itself can't be reached from this machine.Detekt/Spotless/Kover/JUnit 5 opt-ins were not exercised in this verification pass (the representative inputs used
static-analysis: lintandcoverage-tool: jacoco/junit: 4, i.e. no opt-ins triggered) — their plugin ids/fallback versions above are unverified locally; confirm them the first time a user actually opts in.The seven
gradle.propertiesflags carried forward from the reference project (android.usesSdkInManifest.disallowed,android.sdk.defaultTargetSdkToCompileSdkIfUnset,android.enableAppCompileTimeRClass,android.builtInKotlin,android.newDsl,android.r8.optimizedResourceShrinking,android.defaults.buildfeatures.resvalues) each print an individualWARNING: The option setting '<flag>' is deprecated ... It will be removed in version 10.0 of the Android Gradle pluginon every build with AGP9.0.1. They don't fail the build and are kept for fidelity to the reference project (which predates this deprecation), but note this explicitly to the user in Step 8 rather than leaving seven silent warnings on every single build — they'll need removing (or explicit new defaults set) before an eventual AGP 10 upgrade.Separately, applying
org.jetbrains.kotlin.android(libs.plugins.kotlin.android) itself now prints⚠️ Deprecated 'org.jetbrains.kotlin.android' plugin usage ... no longer required for Kotlin support since AGP 9.0, recommendingbuilt-in-kotlin: yesinstead. This skill still defaultsbuilt-in-kotlintono(Step 2), matching the reference project and avoiding AGP's still-young built-in-Kotlin path — but both this and thegradle.propertiesdeprecations above point the same direction, and a future revision of this skill should reconsider defaultingbuilt-in-kotlin: yesonce that path is less new.android:theme="@style/Theme.App"still neededpackage="<application-id>"removed from the manifest's<manifest>tag (see Step 5'sAndroidManifest.xmlnote) — confirmed this produces a "Setting the namespace via the package attribute ... is no longer supported, and the value is ignored" warning on every build otherwise; the attribute contributes nothing onceandroid.namespaceis set inbuild.gradle.kts.androidx.activity:activity-composeandandroidx.compose.ui:ui-tooling-previewmust both be declared explicitly (see Step 5's dependency note) — omitting either fails Kotlin compilation (Unresolved reference 'setContent'/Unresolved reference 'Preview') even with the Compose BOM andandroidx.compose.material3:material3both present; neither pulls the other in transitively. This was the one real (not just deprecation-warning) defect found in this skill's first draft, fixed before this verification pass's second, successful run.org.gradle.configuration-cache=true(fromgradle.properties) is compatible with:app:assembleDebug/:app:testDebugUnitTest/:app:lint/:app:assembleRelease/:app:createDebugUnitTestCoverageReport, all confirmed ("Configuration cache entry stored" on every run) — but not withpublishAndReleaseToMavenCentral- style tasks in general (irrelevant here, since this skill never generates a publishing task) — if a future opt-in ever adds one that warns about configuration cache, pass--no-configuration-cachefor that specific task the same way the reference project'spublish.ymldoes for its library module.UI-contract additions, verified in
$TMPDIR/iru-verify/android/app-ui/{compose,views}(outside this repository): bothuivariants were generated straight from Step 5's templates with the fallback versions (AGP9.0.1, Kotlin2.3.10, Compose BOM2026.02.01, Robolectric4.16.1, Robolectricsdk=35) plus Roborazzi1.76.0, on Gradle9.8.1and JDK 21:./gradlew :app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin(plus:app:lintDebugforviews) passed, and the sample UI test wrote exactly four captures —.ui-screenshots/component/Greeting/default--{ltr,rtl}--{light,dark}.png(compose) and.ui-screenshots/screen/MainActivity/default--…png(views) — with the ATF check passing atenforce. The same helper and tests also passed on AGP9.4.1/Kotlin2.4.21/BOM2026.09.00/Robolectric4.17(which needscompileSdk = 37for that BOM, and prints a deprecation for the v1createComposeRule). Robolectricsdk=36on JDK 21 fails Robolectric'sFileDescriptorinterceptor (IllegalAccessException … jdk.internal.access) — keeprobolectric.propertiesone level behind, or addall { it.jvmArgs("--add-exports=java.base/jdk.internal.access=ALL-UNNAMED") }insidetestOptions.unitTests. The instrumented tests were compile-checked only (no emulator); Compose'senableAccessibilityChecks()needs an API 34+ device and is a no-op guard below that.
Finish with an explicit Warn explicitly block:
- The user must review every generated file before building, committing, or distributing. In particular:
confirm the license choice matches an actual repository-root
LICENSEfile (or that none is intended — suggestiru-check-licenseotherwise), replace the placeholder launcher icon/monogram vector art with real brand art before any real release, and never commit a real keystore —*.jks/*.keystoremust stay gitignored (iru-setup-android-gitignorecovers this) andSIGNING_STORE_FILEshould point at a keystore kept outside version control (a CI secret decoded at build time, or a local, gitignored file). app/google-services.json(only relevant whendistribution: internal) must stay gitignored — it carries project-specific Firebase identifiers that shouldn't be committed, even though they aren't traditional secrets.- If
sonar: cloudorsonar: self-hostedwas chosen, aSONAR_TOKENrepository secret and an actual SonarCloud/ SonarQube project matchingsonar.projectKeystill need to exist before a CI-driven./gradlew :app:sonarscan will succeed — this skill only writes thesonar {}block, it never runs a scan itself. - If
distribution≠none, the relevant credentials (FirebaseFIREBASE_APP_ID+ service-account JSON forinternal; Google Play service-account JSON forstore, however it's invoked) must exist as CI secrets — or be available locally — before any upload succeeds; this skill never collects or stores them. - Unverified locally, and never a blocker:
connectedAndroidTest(needs a running emulator/device — none available here),appDistributionUploadRelease/publishBundle(need real Firebase/Google Play credentials), and a genuinely signedassembleRelease/bundleRelease(needs a real keystore) — this skill only confirms the unsigned path builds. Everything else in the "Known quirks" section above was run for real while building and verifying this skill.