Android Project Folder Structure: The Ultimate Guide - dhruvin207/android-common-utils GitHub Wiki

🎯 Goal of This Guide

This guide is designed to help you master the art of structuring your Android project folders. Whether you're building a small app or a large-scale enterprise application, the way you organize your code can have a significant impact on the maintainability, scalability, and clarity of your project. By following the principles outlined in this guide, you'll learn how to create a clean, efficient, and future-proof architecture for your Android apps.

πŸš€ Benefits and Usefulness

πŸ“‚ Well-Organized Codebase

A clear and logical structure makes it easy for developers to navigate the project, reducing the time spent searching for files and understanding the codebase.

πŸ”„ Scalability

By adopting a feature-based and layer-based structure, your project can grow without becoming cluttered or difficult to manage, ensuring that your application remains maintainable even as its complexity increases.

πŸ§ͺ Improved Testability

Avoiding common mistakes, such as using platform-specific dependencies in the domain layer, ensures that your code remains testable and reusable, leading to higher quality software.

πŸ› οΈ Easier Maintenance

With a well-structured project, adding new features, fixing bugs, and refactoring code becomes much simpler, allowing for quicker iterations and more robust applications.


πŸ“¦ What Makes a Good Package Structure?

A good package structure isn't just about organizationβ€”it's about creating a robust architecture that supports the growth and evolution of your project. Here’s what you should consider:

  1. Clarity and Intuitiveness 🎯: Your structure should be self-explanatory, making it easy for new team members to find what they need.
  2. Scalability πŸ“ˆ: The structure should accommodate future growth without becoming cluttered.
  3. Ease of Migration πŸ”„: Your structure should facilitate a smooth transition to a multi-module project if needed.

🧩 Feature-Based Package Structuring

The first step in organizing your project is dividing it into features. A feature represents an isolated unit in your application that performs a specific function. This approach ensures that each feature is self-contained, making your project easier to manage and scale.

Example:

  • Authentication: Contains the registration, login, and password recovery screens.
  • Home: Includes the main feed, post details, and commenting functionalities.
  • Profile: Manages user profile viewing and editing.

Each feature should be a root module in your package structure, which can later be migrated into separate Gradle modules if needed.

πŸ—οΈ Layer-Based Package Structuring

Inside each feature package, further divide your code into layers:

  1. Presentation Layer 🎨: Contains all UI-related components like activities, fragments, and view models.
  2. Domain Layer 🧠: Handles business logic, such as use cases and validation logic.
  3. Data Layer πŸ“Š: Manages data sources, repositories, and API interfaces.

Example:

com.example.app.feature.auth
β”œβ”€β”€ presentation
β”‚   β”œβ”€β”€ login
β”‚   β”‚   β”œβ”€β”€ LoginActivity.kt
β”‚   β”‚   β”œβ”€β”€ LoginViewModel.kt
β”‚   β”‚   └── LoginEvent.kt
β”‚   └── register
β”‚       β”œβ”€β”€ RegisterActivity.kt
β”‚       └── RegisterViewModel.kt
β”œβ”€β”€ domain
β”‚   β”œβ”€β”€ AuthRepository.kt
β”‚   └── ValidateUserUseCase.kt
└── data
    β”œβ”€β”€ AuthApi.kt
    └── AuthRepositoryImpl.kt

This approach keeps related components together, making it easier to maintain and test each feature independently.

πŸ“š Type-Based vs. Context-Based Packaging

When organizing your domain and data layers, you have two primary approaches:

  1. Type-Based Packaging πŸ“¦: Group classes by their type, such as models, repositories, and use cases.
  2. Context-Based Packaging 🧩: Group classes by the context they operate in, such as user-related functionalities.

Type-Based Example:

  • Models: Contains all data models.
  • Repositories: Contains all repository interfaces and implementations.
  • Use Cases: Contains all use cases.
com.example.app.feature.auth
β”œβ”€β”€ domain
β”‚   β”œβ”€β”€ models
β”‚   β”‚   └── User.kt
β”‚   β”œβ”€β”€ repositories
β”‚   β”‚   └── AuthRepository.kt
β”‚   └── usecases
β”‚       └── ValidateUserUseCase.kt
β”œβ”€β”€ data
β”‚   β”œβ”€β”€ models
β”‚   β”‚   └── UserDTO.kt
β”‚   β”œβ”€β”€ repositories
β”‚   β”‚   └── AuthRepositoryImpl.kt
β”‚   └── apis
β”‚       └── AuthApi.kt

Context-Based Example:

  • User: Contains user-related models, repositories, and use cases.
com.example.app.feature.auth
β”œβ”€β”€ domain
β”‚   └── user
β”‚       β”œβ”€β”€ User.kt
β”‚       β”œβ”€β”€ AuthRepository.kt
β”‚       └── ValidateUserUseCase.kt
β”œβ”€β”€ data
β”‚   └── user
β”‚       β”œβ”€β”€ UserDTO.kt
β”‚       β”œβ”€β”€ AuthRepositoryImpl.kt
β”‚       └── AuthApi.kt

Which to Choose?

  • Large Projects: Context-based packaging often scales better as it keeps related classes together.
  • Smaller Projects: Type-based packaging may be more intuitive and easier to navigate.

πŸ”„ Handling Shared Classes

In any project, you may have classes that are used across multiple features. These should be placed in a core package to ensure reusability and avoid redundancy.

Example:

com.example.app.core
β”œβ”€β”€ presentation
β”‚   └── utils
β”‚       └── UiHelper.kt
β”œβ”€β”€ domain
β”‚   └── models
β”‚       └── User.kt
└── data
    └── db
        └── AppDatabase.kt

This approach centralizes shared components, making them easily accessible to all features.

πŸ› οΈ Migrating to a Multi-Module Structure

As your project grows, you might need to migrate to a multi-module structure for better modularity and reusability.

  • Start with a Single Module: Begin with a monolithic module that contains all your features.
  • Structure Packages as Modules: Organize your packages as if they were separate modules.
  • Seamless Migration: When needed, you can easily extract these packages into independent Gradle modules.

❌ Common Mistakes in Clean Architecture

When implementing Clean Architecture in Android projects, developers often encounter several common pitfalls. Understanding these mistakes and knowing how to avoid them is crucial for maintaining a clean, testable, and scalable codebase.

🚫 Using Android-Specific Dependencies in Domain Layer

One of the most common mistakes is using Android-specific or third-party dependencies in the domain layer. The domain layer should be independent and reusable across different projects, which means it should not rely on platform-specific classes like Patterns from the Android framework.

Example

Consider a ValidateEmailUseCase class that uses Patterns to validate an email:

class ValidateEmailUseCase {
    fun execute(email: String): Boolean {
        return Patterns.EMAIL_ADDRESS.matcher(email).matches()
    }
}

This is problematic because Patterns is an Android-specific class, which makes the domain layer dependent on the Android framework.

The Solution

Introduce an abstraction in the domain layer and implement the Android-specific logic in the data layer:

interface EmailValidator {
    fun isValid(email: String): Boolean
}

class AndroidEmailValidator : EmailValidator {
    override fun isValid(email: String): Boolean {
        return Patterns.EMAIL_ADDRESS.matcher(email).matches()
    }
}

In your domain layer, inject this validator:

class ValidateEmailUseCase(private val emailValidator: EmailValidator) {
    fun execute(email: String): Boolean {
        return emailValidator.isValid(email)
    }
}

This approach ensures that the domain layer remains platform-independent and easily testable.

⚠️ Over-Abstraction

Another common mistake is unnecessary abstraction, such as creating interfaces or abstract classes for components that will only have a single implementation.

Example

Consider a BookMapper class that converts BookDTO to Book:

interface BookMapper {
    fun toBook(dto: BookDTO): Book
    fun toBookDTO(book: Book): BookDTO
}

class BookMapperImpl : BookMapper {
    override fun toBook(dto: BookDTO): Book {
        return Book(dto.id, dto.title, dto.releaseDate)
    }

    override fun toBookDTO(book: Book): BookDTO {
        return BookDTO(book.id, book.title, book.releaseDate)
    }
}

This abstraction adds unnecessary complexity without providing any real benefits since there’s only one implementation.

The Solution

Remove unnecessary interfaces and simplify your code:

class BookMapper {
    fun toBook(dto: BookDTO): Book {
        return Book(dto.id, dto.title, dto.releaseDate)
    }

    fun toBookDTO(book: Book): BookDTO {
        return BookDTO(book.id, book.title, book.releaseDate)
    }
}

Or, even better, use Kotlin extension functions to make the mapping more concise:

fun BookDTO.toBook(): Book {
    return Book(id, title, releaseDate)
}

fun Book.toBookDTO(): BookDTO {
    return BookDTO(id, title, releaseDate)
}

🚧 Improper Package Structure

A common mistake in Clean Architecture is organizing packages by layers (data, domain, presentation) at the root level of the project. This approach may work for smaller projects but becomes problematic as the application scales. As your project grows, having all your data, domain, and presentation classes in these broad categories can lead to bloated packages, making it difficult to manage and navigate the codebase.

Example

com.example.app
β”œβ”€β”€ data
β”‚   β”œβ”€β”€ BookRepository.kt
β”‚   └── ApiService.kt
β”œβ”€β”€ domain
β”‚   β”œβ”€β”€ Book.kt
β”‚   └── GetBooksUseCase.kt
└── presentation
    β”œβ”€β”€ BookListActivity.kt
    └── BookViewModel.kt

In this structure, all domain-related logic is crammed into a single package, making it hard to locate specific functionality and leading to potential conflicts as the project grows.

The Solution

For large-scale projects, it’s crucial to adopt a feature-based package structure where each feature is self-contained, including its own data, domain, and presentation layers. This not only helps in organizing the code better but also makes it easier to scale, test, and maintain.

Example

com.example.app.feature.book
β”œβ”€β”€ data
β”‚   β”œβ”€β”€ BookRepository.kt
β”‚   └── ApiService.kt
β”œβ”€β”€ domain
β”‚   β”œ

── Book.kt
β”‚   └── GetBooksUseCase.kt
└── presentation
    β”œβ”€β”€ BookListActivity.kt
    └── BookViewModel.kt

This structure ensures that all components related to a specific feature are grouped together, reducing the complexity as the project scales. Each feature can be developed, tested, and maintained independently, leading to a cleaner and more modular codebase.

πŸŽ‰ Conclusion

A well-structured Android project is crucial for long-term success. By adopting feature-based and layer-based packaging, and choosing the right approach for context-based or type-based organization, you can create a scalable, maintainable, and intuitive project structure. Additionally, by avoiding common Clean Architecture mistakes like over-abstraction and improper dependency placement, you can ensure that your project remains clean, testable, and easy to scale.


Happy Coding! πŸ‘¨β€πŸ’»πŸ‘©β€πŸ’»