Android Project Structure: A Practical Guide
Learn where Android code, resources, Gradle files, and tests belong—and how the structure scales as an app grows.
An Android project feels much easier to work with once each folder has a clear job. The structure is not just a convention: Gradle uses it to decide what to compile, Android uses it to package resources, and the test system uses it to separate unit tests from device tests.
The project-level map
Most modern Android projects use Gradle with Kotlin DSL. A small application commonly starts with a structure like this:
weather-app/
├── app/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ ├── test/
│ └── androidTest/
├── build.gradle.kts
├── settings.gradle.kts
├── gradle.properties
├── gradlew
├── gradlew.bat
└── gradle/
└── libs.versions.toml
The root folder contains build configuration shared by the project. The app folder is an Android application module. Larger products may have several modules such as :app, :core, and :feature:payments, but the same ideas apply to each module.
The app module
The module-level build.gradle.kts describes how the application is built. It usually contains the Android SDK settings, application ID, build types, product flavors, and dependencies.
android {
namespace = "dev.example.weather"
compileSdk = 35
defaultConfig {
applicationId = "dev.example.weather"
minSdk = 26
targetSdk = 35
}
}
Keep module configuration focused on build concerns. Application behavior belongs under src/main, not inside Gradle scripts.
src/main: the application source set
The main source set is the code and resources shared by the normal application builds.
app/src/main/
├── AndroidManifest.xml
├── java/dev/example/weather/
│ ├── MainActivity.kt
│ ├── data/
│ ├── domain/
│ └── ui/
└── res/
├── drawable/
├── mipmap-anydpi-v26/
├── navigation/
├── values/
└── xml/
The java directory can contain Kotlin or Java files. The folder name is historical; it does not mean that Kotlin files are in the wrong place. Organize packages by responsibility rather than by file type once the application becomes more than a few screens.
The manifest
AndroidManifest.xml declares components and capabilities that Android needs before the application runs. Activities, services, broadcast receivers, permissions, and application-level settings belong here.
The manifest should describe the app’s contract with the operating system. It should not become a general configuration file for values that belong in Kotlin or resources.
Resources
Resources are compiled separately from Kotlin code and can be selected by device configuration.
drawable: vector and bitmap drawables.mipmap: launcher icons.layout: XML layouts when using the View system.navigation: navigation graphs for XML-based navigation.values: strings, colors, dimensions, and themes.xml: other declarative configuration files.
Use resource references such as R.string.app_name instead of hard-coding user-facing text. This keeps localization and alternate configurations possible later.
Tests and source sets
Android separates tests based on where they run:
app/src/
├── test/
│ └── ... # JVM unit tests
└── androidTest/
└── ... # emulator or device tests
Use src/test for fast tests that do not need Android framework classes: parsing, validation, reducers, use cases, and other business rules. Use src/androidTest when a test needs a device, emulator, Android context, database, UI, or instrumentation.
Keeping most logic testable on the JVM makes the feedback loop much faster. A screen test can then focus on integration details instead of repeating every business-rule test.
Build types and flavors
Gradle can combine build types and product flavors into variants such as debug, release, demoDebug, or stagingRelease.
app/src/
├── main/ # shared source
├── debug/ # debug-only source and resources
├── release/ # release-only source and resources
└── demo/ # demo flavor source and resources
When the same path exists in multiple source sets, Gradle chooses the more specific version for the active variant. This is useful for debug logging, mock services, or flavor-specific branding, but too many overrides can make the project difficult to reason about. Prefer shared code with small, explicit differences.
A structure that scales
A practical package layout for a feature-oriented app might look like this:
dev.example.weather/
├── core/
│ ├── network/
│ └── database/
├── feature/
│ ├── forecast/
│ │ ├── ForecastScreen.kt
│ │ ├── ForecastViewModel.kt
│ │ └── ForecastRepository.kt
│ └── settings/
└── MainActivity.kt
The important rule is ownership: the feature that owns a screen should also own its UI state and feature-specific logic. Shared modules should contain genuinely shared capabilities rather than becoming a dumping ground.
Common mistakes
- Putting API keys in
BuildConfigand committing them to the repository. Build configuration is not a secret store. - Mixing device tests into
src/test, which makes fast unit-test runs fail or become needlessly coupled to Android. - Creating a huge
utilspackage instead of placing helpers near the feature that owns them. - Keeping every feature in
MainActivity, which makes navigation and state ownership harder to follow. - Adding a new module before there is a clear boundary. Modules are useful when they enforce ownership or improve build times.
The directory tree is a map of responsibilities. If a file is hard to place, that is often a signal that its responsibility is not clear yet.
A simple workflow
When adding a feature, start with its package under src/main, add user-facing values to res/values, add a unit test under src/test, and only use src/androidTest for behavior that truly needs Android. Keep the Gradle file limited to build configuration.
Once these boundaries become habits, navigating a new Android codebase is much faster—and the structure continues to communicate how the app works as the team and feature set grow.