Skip to content

Latest commit

 

History

History
189 lines (154 loc) · 11.2 KB

File metadata and controls

189 lines (154 loc) · 11.2 KB

Apk Analyzer Agent Instructions

Android app for inspecting installed apps and APK files — permissions, components, certificates, device-wide statistics. Multi-module, Kotlin, Compose, Hilt.

Every module has its own AGENTS.md. Read it before working inside that module instead of re-deriving the module's structure.

Non-Negotiables

  • Kotlin only. Jetpack Compose only — no XML layouts. Hilt only — no Dagger or Koin.
  • Coroutines and flows only for concurrency. Never Thread, Executor, or runBlocking.
  • Don't add a dependency. gradle/libs.versions.toml is the only source of coordinates and versions; if a new library seems necessary, ask first.
  • Never write comments or KDoc — not even when the WHY seems non-obvious. Use self-documenting names and structure. The only exception is an explicit request for a comment in that instance.
  • Never hardcode a user-facing string in a Composable. Use stringResource backed by res/values/strings.xml in the module that owns the UI.
  • Never hardcode SDK levels or the JVM toolchain in a module's build.gradle.kts — they come from AndroidSdk.kt and Kotlin.kt.
  • Commits are authored as the human user only. Never add Co-Authored-By: Claude, Claude-Session:, or any AI co-author trailer. See the git-commit-author skill.

Verifying a Change

When Run
Iterating on one module ./gradlew :feature:apps:impl:compileDebugKotlin
Before committing ./gradlew spotlessApply
Whole-app check (what CI gates on) ./gradlew spotlessCheck detektDebug :build-logic:convention:detektMain lintDebug :app:assembleDebug
After changing a context file, skill, adapter, or the module graph ./gradlew validateAgentContext

A successful compile is not proof a Compose layout is correct. For visual or layout changes, use the run-app skill and look at it on a device. Two failure modes that pass every gate: text that wraps to three lines inside a fixed-width column, and two indicators that disagree because each derived its own answer.

spotlessCheck does not flag unused imports. Deleting the last use of something does not delete its import, and no gate will tell you — check by hand.

Module Rules

Modules live under app/, core/<name>/, and feature/<name>/{api,impl}/; settings.gradle.kts is the authoritative list.

  • feature/*/api — depends on nothing, holds only @Serializable NavKeys and the tab-label string.
  • feature/*/implapi(projects.feature.<name>.api) plus whichever core modules it needs.
  • core/* — may depend on other core modules. Never depends on a feature module.
  • A feature never depends on another feature's impl. Cross-feature navigation goes through the target's api module.
  • app — wiring only: the launcher Activity, the external-APK document Activity, nav hosts, and app-scoped Hilt bindings. Put no feature logic here.
  • Declare dependencies with typesafe accessors (projects.core.appPermissions), never project(":core:app-permissions"). Apply plugins with alias(libs.plugins.apkanalyzer.*).
  • A module's package matches its directory with hyphens removed: core/user-preferencescore.userpreferences, feature/app-detail/implfeature.appdetail.impl. app uses the root package sk.styk.martin.apkanalyzer with no suffix.
  • feature:browse is a stub — a placeholder screen with no ViewModel or logic. Don't assume it works.

Architecture

ViewModels

  • Extend ViewModel() directly. No base class.
  • Expose exactly one val state: StateFlow<FeatureState> and one fun onAction(action: FeatureAction) with a when dispatch. No other public methods.
  • One-shot events go through Channel<Event>(Channel.BUFFERED) exposed as receiveAsFlow() — never as state.
  • Statesealed interface or @Immutable data class. Never holds lambdas.
  • Eventsealed interface. ViewModel→UI signals (navigation, toasts, system intents).
  • Actionsealed interface. UI→ViewModel intents.
  • Model back navigation as both an Action and an Event so the ViewModel never touches Navigator.
  • In Composables: collectAsStateWithLifecycle() for state, LaunchedEffect for events.

Data Layer

  • Repositories and Managers are a public interface plus an internal Impl in the same module, bound with Hilt @Binds and scoped @Singleton.
  • Interface methods never throw. Return Result<T>, a nullable T?, or an empty collection.
  • Inject DispatcherProvider and switch with flowOn(dispatcherProvider.default()) or withContext(dispatcherProvider.io()). Never hardcode Dispatchers.IO.

Hilt

  • @HiltViewModel + @Inject constructor. When a ViewModel needs a runtime parameter, use @HiltViewModel(assistedFactory = VM.Factory::class) + @AssistedFactory + @AssistedInject.
  • Modules are @Module @InstallIn(SingletonComponent::class) — an interface with @Binds for interfaces, a class with @Provides only for platform types.
  • Prefer constructor injection over module @Provides.

Navigation

  • Navigation 3 only. No legacy Jetpack Navigation.
  • NavKeys are @Serializable and implement NavKey. data class when carrying parameters, data object when not. Keys reachable from other features go in feature/*/api; keys internal to one feature go in that feature's impl/navigation/.
  • Register screens via EntryProviderScope<NavKey>.<feature>Entries(navigator: Navigator) in feature/*/impl/navigation/, then wire that call into the entryProvider { } block in app/src/main/kotlin/sk/styk/martin/apkanalyzer/ui/ApkAnalyzerApp.kt. A screen that isn't wired there is unreachable.
  • Transitions: bottomEntryMetadata() / slideFromEndEntryMetadata() from sk.styk.martin.apkanalyzer.core.uilibrary.animation.
  • Multi-stack state lives in :core:navigation — see core/navigation/AGENTS.md.

Compose

  • Feature modules must never import androidx.compose.material3. Use Compose foundation APIs plus the wrappers in :core:ui-library. If a component isn't wrapped yet, wrap it there first — see the create-compose-component skill. (app uses material3 directly for Scaffold and theme plumbing; that is the only exception.)
  • Colors and type come from AppTheme.colors / AppTheme.typography, icons from ApkAnalyzerIcons. Never hardcode a color or reach for MaterialTheme.colorScheme outside :core:ui-library.
  • Check the component inventory in core/ui-library/AGENTS.md before calling a component — names don't always match file names.
  • Every list property in State classes and Composable parameters is an ImmutableList from kotlinx.collections.immutable. @Immutable on State data classes; @Stable on non-data classes used as Composable parameters.
  • Every file with @Composable functions has @Preview functions: private, suffixed Preview, wrapped in ApkAnalyzerTheme { }, with realistic sample data. Preview the stateless content composable, never the ViewModel-dependent screen.
  • Composable callbacks are present tense — onClick, onSelectItem, onBack. Never onClicked, onItemSelected, onBackPressed.
  • LazyColumn item keys must be unique across the whole list, not per section. A sectioned list keyed on an item identifier crashes when the same identifier lands in two sections. Deduplicate in the ViewModel rather than compounding the key with the section.
  • Two indicators of the same fact must come from one predicate. A verdict line and a row of icons that each recompute "is this a problem" will eventually contradict each other on screen.
  • A row that has nothing to show on tap means the item sheet is missing, not that the idiom is optional. Don't neutralise the tap (indication = null, a no-op onClick) to hide a dead affordance — every list row in app detail is tap = explain, long-press = copy.

Conventions

  • data object, not plain object, for sealed interface members.
  • A nullable primitive that encodes a variant or a third state is a type in disguise. Boolean? meaning yes/no/unknown, or a String? whose null marks "a different kind of thing", belongs in an enum or a sealed interface. Feature (Hardware vs OpenGlEs) and FeatureAvailability in core:apps are the worked examples — both replaced exactly that shape.
  • Extract a shared helper when the second consumer appears — and grep for a third before writing your own. Duplicated composables here have reached three identical copies before anyone noticed.
  • No wildcard imports.
  • Prefer private; internal for module-visible; public only for actual public API.
  • Logger from core.common.logger, never raw Timber: Logger.d("Tag", "msg"), Logger.e("Tag", throwable, "msg"). Declare private const val TAG at file level.
  • @Serializable for nav keys and new models. Parcelize only for existing Android-specific data already passed through intents/bundles.
  • No test infrastructure or test dependencies exist here. Don't add tests, test dependencies, or test source sets unless explicitly asked.

User-Facing Copy

Write for non-technical users who understand Android basics. Active voice, present tense, sentence case. Name the concrete thing rather than gesturing at it — "Shows which apps use the Camera permission", not "View permission information". Never surface class names, internal identifiers, or Android API names in a user-facing string.

Skills

Read the relevant skill in .claude/skills/ before starting a matching task — skills hold the step-by-step procedures this file deliberately omits. Each skill's frontmatter states when it applies.

create-feature-module, create-core-module, create-compose-component, implement-navigation, spotless-fix, git-commit-author, setup-local-tools, analyze-ci-failure, run-app, navigate-app-adb, sync-design-changes.

All are shared by Claude and Copilot except sync-design-changes, which needs Claude's DesignSync tool.

Shared AI Context

  • AGENTS.md files are canonical. Put guidance in the closest relevant AGENTS.md; never copy it into a tool-specific file.
  • CLAUDE.md files contain exactly @AGENTS.md and nothing else.
  • .github/copilot-instructions.md is the Copilot adapter; Copilot also reads nested AGENTS.md directly. .claude/skills/ is shared by both tools — never mirror skills into .github/skills/, .github/prompts/, or .agents/skills/.
  • validateAgentContext enforces AGENTS/CLAUDE pairing, per-module coverage, skill frontmatter, duplicate skill locations, and that every local markdown link resolves.

Production References

Read these instead of copying a template into a new file: