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:
- Clarity and Intuitiveness π―: Your structure should be self-explanatory, making it easy for new team members to find what they need.
- Scalability π: The structure should accommodate future growth without becoming cluttered.
- 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:
- Presentation Layer π¨: Contains all UI-related components like activities, fragments, and view models.
- Domain Layer π§ : Handles business logic, such as use cases and validation logic.
- 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:
- Type-Based Packaging π¦: Group classes by their type, such as models, repositories, and use cases.
- 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! π¨βπ»π©βπ»