App Platform Module Structure
CommunityvRallev
Design and verify App Platform module boundaries. Use when placing shared APIs, implementations, internal code, fakes, robots, or app assembly; adding Gradle modules or dependencies; enabling module structure checks; or fixing forbidden dependency errors.
Overview
- Repository
- vRallev/app-platform
- Publisher
- vRallev
- Stars
- 331
- Forks
- 32
- Bundled files
- 1
- License
- Apache-2.0
- Updated
- Nov 14, 2026
Installation
tmp=$(mktemp -d)
git clone --depth 1 https://github.com/vRallev/app-platform.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/skills/app-platform-module-structure" ~/.claude/skills/app-platform-module-structure
rm -rf "$tmp"tmp=$(mktemp -d)
git clone --depth 1 https://github.com/vRallev/app-platform.git "$tmp"
mkdir -p ~/.codex/skills
cp -R "$tmp/skills/app-platform-module-structure" ~/.codex/skills/app-platform-module-structure
rm -rf "$tmp"Install the App Platform Module Structure agent skill from https://github.com/vRallev/app-platform/tree/main/skills/app-platform-module-structure into this project's skills folder.Instructions
App Platform Module Structure
App Platform applies dependency inversion at the Gradle module boundary. Consumers compile against small public contracts, concrete implementations stay hidden, and the application chooses implementations at its assembly boundary. This feature is optional and separate from scopes, presenters, renderers, and DI.
Follow the consuming project's App Platform version, target setup, and Gradle conventions. Before editing, read settings.gradle or settings.gradle.kts, repository instructions, and nearby module build files. Examples omit imports.
Choose a boundary
First identify the owning library, its consumers, the stable contract they need, and the concrete details they should not see. Also check whether the library has platform code, alternate implementations, reusable fakes, or reusable UI robots.
Choose the smallest boundary that preserves dependency inversion. Do not create a module only because several files share a category. A library can have only :public when its reusable code needs no hidden implementation, such as utilities or shared UI components.
Module types
Keep sibling modules under one library path, such as :account:public and :account:impl.
| Module | Put here | Used by |
|---|---|---|
:public |
Shared contracts, models, and code intentionally safe to expose | Other libraries, implementations, tests, and apps |
:impl or :impl-name |
Concrete implementations, presenter and renderer implementations, DI bindings, and platform details | Final app assembly; zero or more per library |
:internal |
Private implementation-side code shared by sibling non-public modules | Non-public modules in the same library; rarely app assembly |
:testing |
Reusable fakes and test helpers for the public API | Tests and other test-only modules |
:*-robots |
Reusable UI or integration-test robots; usually :impl-robots |
UI or integration tests and other robot modules |
:app or another recognized app path |
Final composition, selected implementations, and platform host code | The final binary |
Keep implementation-private models in :impl. Keep platform launcher, manifest, packaging, window, activity, and native host code in the platform app shell or the target source set that needs it.
Dependency rules
- Among structured modules,
:publicdepends only on other:publicmodules. Keep implementation and platform types out of public signatures. - Feature implementations depend on other libraries' public contracts. Final app assembly selects and imports their
:implmodules. :implto:impldependencies are forbidden by default. A library can share private code through:internal, or allow same-library implementation dependencies so one implementation builds on another.:internalstays on the implementation side of its library. A sibling:publicmodule cannot import it. App assembly may import it, but ordinary cross-library consumers may not.- Ordinary production modules add
:testingonly to test configurations and robots only to UI or integration-test configurations. Test-only:testingand robot modules may use:testingfrom their main source sets. Robot modules may also depend on other robot modules. None belongs on the final runtime classpath. - App modules may import
:impland:internalmodules. Their production classpaths must still exclude:testingand robot modules.
Gradle's api versus implementation choice is separate. Use api only when a dependency's types are part of the exposed API.
If many modules need a concrete type, stop and define or revise its public contract. Do not move test helpers into production code to work around visibility.
Enable the structure
Enable the option in each participating module, directly or through the consuming project's convention plugin:
plugins {
id("software.ralf.app.platform")
}
appPlatform {
enableModuleStructure(true)
}Enabling it in one module does not configure its consumers. For each enabled module, the plugin:
- Checks that the module name is a supported type.
- Adds its sibling
:publicdependency to:impl,:internal,:testing, and robot modules when that module exists. - Adds the matching implementation to an
:impl-robotsmodule as an implementation dependency. - Supplies default archive names and Android namespaces while preserving explicit values.
- Registers
checkModuleStructureDependenciesand connects it to Gradle'schecklifecycle when available.
Automatic Android and Android-KMP namespaces require a GROUP Gradle property when no explicit namespace is set. Public modules omit .public from the generated namespace, and hyphens become dots.
Do not repeat automatic sibling dependencies unless the consuming build has a documented reason.
Assemble the application
The app boundary imports concrete feature modules and builds the root graph, scope, template, and platform entry points. When it relies on App Platform's default implementations, enable them there:
appPlatform {
addImplModuleDependencies(true)
}This adds App Platform implementation artifacts for enabled framework features. It does not discover or import the application's feature :impl modules.
A shared KMP application assembly may use a path such as :app-framework:impl. Its leaf name still makes it an implementation module, even though it intentionally imports feature implementations. Keep the module-structure defaults but disable dependency enforcement only at that composition root:
appPlatform {
enableModuleStructure {
enableDependencyCheck(false)
}
addImplModuleDependencies(true)
}Fix ordinary feature dependency violations instead of disabling their checks.
Some libraries use :internal for code shared by implementations. Others let one :impl module build on another and avoid a separate internal module. Enable that choice on the consuming implementation:
appPlatform {
enableModuleStructure {
allowLibraryImplToImplDependencies(true)
}
}This allows project implementation dependencies only within the same library. Cross-library and external implementation dependencies remain forbidden.
Add or split a library
Map current consumers and choose the smallest stable contract.
Add the required modules to Gradle settings, using one library parent:
include(":account:public") include(":account:impl") include(":account:testing") include(":account:impl-robots")Put contracts and shared models in
:public. Keep public APIs free of implementation and platform types.Put concrete code in one or more
:implmodules. Use target source sets inside an implementation for platform-specific code; use named implementation modules when apps or vendors select different implementations.Choose how sibling implementations share code: add
:internal, or enable same-library implementation dependencies and reuse an implementation module.Put reusable fakes in
:testingand reusable UI robots in a robot module. Keep one-off test helpers beside their tests.Select implementations and assemble DI at the app boundary.
Copy target and App Platform feature configuration from the closest matching modules instead of enabling every feature everywhere.
A typical graph is:
:checkout:impl -> :account:public
:app -> :checkout:impl, :account:impl
:checkout tests -> :account:testing
Test and verify
Run the consuming project's focused compile and test tasks for each changed module, then run:
./gradlew checkModuleStructureDependenciesThe aggregate task checks supported production compile classpaths across Android, JVM, and KMP targets. It also checks Android and JVM test fixtures. Public test fixtures may use :public and :testing, but not :impl.
When changing :public, run the project's API checks. When moving DI contributions or implementation selection, compile the real app graph and platform entry point. Recheck Gradle settings after adding or renaming modules.
The dependency task checks declared dependencies whose names follow the module structure. It does not prove that public APIs are well designed, that transitive dependencies are harmless, or that source-set boundaries are correct. Review exported signatures and the effective dependency graph as well.
Public docs: module structure, setup, and testing.
Bundled files
FAQ
What does App Platform Module Structure do?
Design and verify App Platform module boundaries. Use when placing shared APIs, implementations, internal code, fakes, robots, or app assembly; adding Gradle modules or dependencies; enabling module structure checks; or fixing forbidden dependency errors.
Who made App Platform Module Structure?
vRallev (a community member) publishes it in the vRallev/app-platform repository on GitHub.
Is App Platform Module Structure free?
Yes. App Platform Module Structure is open source, published by vRallev under the Apache-2.0 license.
What comes with App Platform Module Structure?
Its instructions and 1 file the agent reads only when a task needs them, such as references, examples or scripts.
Which agents can use App Platform Module Structure?
Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.
What are agent skills?
An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.
How are agent skills different from MCP servers?
An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.
How can my agent test my Android app?
Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.