A modern Android application following a clean architecture approach with a structured multimodule organization. This project provides a solid foundation for building scalable and maintainable Android applications.
- Architecture Overview
- Module Structure
- Custom Gradle Tasks
- Precompiled Script Plugins
- Dependency Management
- Network Layer
- Getting Started
This project implements a clean architecture approach with a multimodule structure organized by features. The application is divided into the following main module types:
- App: The main application module that connects all the features
- Core: Contains shared functionality across features
- Feature: Feature-specific modules divided into data, domain, and presentation layers
graph TD
App[app]
Libraries[libraries]
subgraph Core[Core]
C1[core-database]
C2[core-data]
C3[core-domain]
C4[core-ui]
end
subgraph Feature[feature]
F1[feature-data]
F2[feature-domain]
F3[feature-presentation]
F4[feature-injection]
end
%% Libraries module dependencies
Feature --> Libraries
Core --> Libraries
App --> Libraries
%% Core module dependencies
C2 --> C1
C2 --> C3
%% Feature module dependencies
F1 --> C2
F2 --> C3
F3 --> C4
F1 --> F2
F3 --> F2
F4 --> F1
F4 --> F2
F4 --> F3
%% App dependencies
App --> Core
App --> F3
App --> F4
Core modules contain functionality shared across multiple features:
- core:data: Network, database access, and common data utilities
- core:database: Database setup, DAOs, and entities
- core:domain: Common domain
- core:ui: Common UI components, themes, and navigation utilities
Each feature is isolated in its own module group with four sub-modules:
- feature:[feature-name]:data: Implements repositories, network services, and data sources
- feature:[feature-name]:domain: Contains business logic, repository interfaces use cases
- feature:[feature-name]:presentation: UI components, ViewModels, and UI states
- feature:[feature-name]:injection: Dependency injection configs for data, domain and presentation modules
A simple module that (I look for a better name) contains common utility classes and doesn't use additional dependencies
Presentation and data modules depend on domain modules. core.data module also depends on database.
The project includes custom Gradle tasks to automate the creation of new modules.
To create a new core module, run:
./gradlew createCoreModule -PmoduleName=yourmodulenameThis task:
- Creates a new core module with the specified name
- Sets up the necessary directory structure
- Creates a basic build.gradle.kts file
- Updates settings.gradle.kts to include the new module
If no module name is specified, it defaults to "newmodule":
./gradlew createCoreModuleTo create a new feature module with data, domain, and presentation layers, run:
./gradlew createFeatureModule -PfeatureName=yourfeaturenameThis task:
- Creates a new feature module with data, domain, presentation and injection sub-modules
- Sets up the necessary directory structure for each sub-module
- Creates build.gradle.kts files with appropriate dependencies
- Updates settings.gradle.kts to include all the new modules
If no feature name is specified, it defaults to "newfeature":
./gradlew createFeatureModuleThe project uses precompiled script plugins in the buildSrc directory to share common build configurations across modules.
This plugin configures basic Android library modules:
plugins {
id("base-library")
}plugins {
id("base-presentation")
}plugins {
id("base-data")
}plugins {
id("base-domain")
}Dependency management is centralized in the buildSrc directory using Kotlin DSL.
- ProjectConfigs.kt: Contains project-level configurations (SDK versions, app ID, etc.)
- DependencyGroups.kt: Organizes dependencies into logical groups
- ProjectExt.kt: Extension functions for dependency declarations
Dependencies are declared in the gradle/libs.versions.toml file, which maintains a centralized list of library versions. This ensures consistent versions across all modules and makes updates easier.
The project defines dependency groups that can be applied together:
// Apply all base dependencies
dependencies {
base()
}
// Apply Android-specific dependencies
dependencies {
baseAndroid()
}
// Apply Compose-related dependencies
dependencies {
compose()
}
//etc....For example, the base() function
in DependencyGroups.kt adds:
- Koin for dependency injection
- Timber for logging
- Kotlin Result for functional error handling
The project includes a custom Retrofit CallAdapter that transforms API responses into a
Result<T, Failure> type using the kotlin-result library. This provides a cleaner way to handle
network responses and errors.
- ResultCallAdapterFactory: Creates a custom CallAdapter for Retrofit that handles API responses.
- ResultCall: Custom Call implementation that transforms responses into Result.
The adapter handles different types of errors:
- HTTP error codes (4xx, 5xx)
- Network failures
- SSL errors
- Parsing errors
Each error is transformed into a user-friendly message using the StrResource.
NetworkResult is a typealias Result<T, Failure>
- Define your API service interface:
interface MyService {
@GET("endpoint")
suspend fun getData(): NetworkResult<ResponseDto>
}- Create the service instance using Retrofit with the ResultCallAdapterFactory ( typically in a Koin module):
single {
get<Retrofit>().create(MyService::class.java)
}- Use the service in your repository:
class MyRepositoryImpl(
private val service: MyService
) : MyRepository {
override suspend fun getData(): Result<DomainModel, Failure> {
return service.getData().map {
it.toDomainModel()
}
}
}- Android Studio (latest version recommended)
- JDK 17
- API key for TMDB (The Movie Database) set as an environment variable:
API_KEY_TMDB=your_api_key
This structured approach ensures a clean separation of concerns and makes your codebase more maintainable and testable.