mutkuensert/MovieDb-Light
A scalable multimodule codebase for big projects which demonstrates that each feature module’s internal layers are fully decoupled, both from each other and from the layers of other features. It includes also relation entities, auto/manuel migrations, cached pagination, precompiled script plugins.
Moviedb Light
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.
Table of Contents
- Architecture Overview
- Module Structure
- Custom Gradle Tasks
- Precompiled Script Plugins
- Dependency Management
- Network Layer
- Getting Started
Architecture Overview
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
Module Structure
Core Modules
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
Feature Modules
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
Libraries Module
A simple module that (I look for a better name) contains common utility classes and doesn't use additional dependencies
Module Dependency
Presentation and data modules depend on domain modules. core.data module also depends on database.
Custom Gradle Tasks
The project includes custom Gradle tasks to automate the creation of new modules.
Creating a Core Module
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 createCoreModuleCreating a Feature Module
To 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 createFeatureModulePrecompiled Script Plugins
The project uses precompiled script plugins in the buildSrc directory to share common build
configurations across modules.
base-library.gradle.kts
This plugin configures basic Android library modules:
plugins {
id("base-library")
}base-presentation.gradle.kts
plugins {
id("base-presentation")
}base-data.gradle.kts
plugins {
id("base-data")
}base-domain.gradle.kts
plugins {
id("base-domain")
}Dependency Management
Dependency management is centralized in the buildSrc directory using Kotlin DSL.
Structure
- 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
Library Versions
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.
Dependency Groups
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
Network Layer
ResultCallAdapterFactory
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.
How It Works
- 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.
Creating and Using a Service
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()
}
}
}Getting Started
Prerequisites
- 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.