A modern Android application template 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
- Dependency Injection
- 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
Each feature follows the principles of clean architecture with:
- Presentation Layer: Contains UI components, ViewModels, and state management
- Domain Layer: Contains business logic, use cases, and entity models
- Data Layer: Handles data operations, repositories, and external service interactions
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:injection: Dependency injection setup using Koin
- core:ui: Common UI components, themes, and navigation utilities
Each feature is isolated in its own module group with three sub-modules:
- feature:[feature-name]:data: Implements repositories, network services, and data sources
- feature:[feature-name]:domain: Contains business logic, entities, and use cases
- feature:[feature-name]:presentation: UI components, ViewModels, and UI states
Core module dependencies:
- core:data depends on core:database
- core:ui is independent
- core:injection depends on all other modules to provide dependency injection
Feature module dependencies:
- feature:[name]:data depends on feature:[name]:domain and core:data
- feature:[name]:domain is independent
- feature:[name]:presentation depends on feature:[name]:domain and core:ui
The project includes custom Gradle tasks to automate the creation of new modules.
To create a new core module, run:
./gradlew createCoreModule -PmoduleName=yourmodulename
This 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 createCoreModule
To create a new feature module with data, domain, and presentation layers, run:
./gradlew createFeatureModule -PfeatureName=yourfeaturename
This task:
- Creates a new feature module with data, domain, and presentation 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 createFeatureModule
The 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")
}
It applies:
- Android library plugin
- Kotlin Android plugin
- Common Android configurations (SDK versions, JVM target, etc.)
- Base dependencies
This plugin configures feature presentation modules with Compose support:
plugins {
id("base-presentation")
}
It applies:
- Android library plugin
- Kotlin Android plugin
- Compose plugin
- UI-related dependencies
- Core UI module dependency
This plugin configures data modules with network support:
plugins {
id("base-data")
}
It applies:
- base-library plugin
- Kotlinx Serialization plugin
- Network-related dependencies
- Core data module dependency
Dependency management is centralized in the buildSrc directory using Kotlin DSL.
- ProjectConfigs.kt: Contains project-level configurations (SDK versions, app ID, etc.)
- Libraries.kt: Provides access to all library dependencies
- 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()
}
// Apply coroutines-related dependencies
dependencies {
coroutines()
}
// Apply unit test dependencies
dependencies {
unitTest()
}
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.
- ResultCallAdapter: Adapts the Call object to return Result type.
- 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 StrResources.
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()
}
}
}
The project uses Koin for dependency injection.
- networkModule: Provides network-related dependencies (Retrofit, OkHttp, etc.)
- databaseModule: Provides database-related dependencies (Room database, DAOs)
- uiModule: Provides UI-related dependencies (Navigator)
- Feature modules: Provide feature-specific dependencies
val myFeatureModule = module {
// Provide service
single { get<Retrofit>().create(MyService::class.java) }
// Provide repository
single<MyRepository> { MyRepositoryImpl(get()) }
// Provide ViewModel
viewModelOf(::MyViewModel)
}
In Composables:
@Composable
fun MyScreen(
viewModel: MyViewModel = koinViewModel()
) {
// Use the ViewModel
}
In Android components:
class MyActivity : ComponentActivity() {
private val navigator: Navigator by inject()
// Use navigator
}
All modules are registered in the Application class:
class MultimoduleTemplate : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@MultimoduleTemplate)
modules(
homeModule,
uiModule,
networkModule,
databaseModule,
moviesModule
// Add your new modules here
)
}
}
}
- 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
- Create the feature modules:
./gradlew createFeatureModule -PfeatureName=profile
- Define the domain model in the domain module:
data class Profile(
val id: String,
val name: String,
// other properties
)
- Define the repository interface in the domain module:
interface ProfileRepository {
suspend fun getProfile(): Profile
}
- Define the API service in the data module:
interface ProfileService {
@GET("profile")
suspend fun getProfile(): NetworkResult<ProfileResponse>
}
- Implement the repository in the data module:
class ProfileRepositoryImpl(
private val service: ProfileService
) : ProfileRepository {
override suspend fun getProfile(): Profile {
return service.getProfile().map {
it.toProfile()
}
}
}
- Create a ViewModel in the presentation module:
class ProfileViewModel(
private val repository: ProfileRepository
) : ViewModel() {
// Implementation
}
- Create the dependency injection module:
val profileModule = module {
single { get<Retrofit>().create(ProfileService::class.java) }
single<ProfileRepository> { ProfileRepositoryImpl(get()) }
viewModelOf(::ProfileViewModel)
}
- Add your module to the Koin setup in the application class.
This structured approach ensures a clean separation of concerns and makes your codebase more maintainable and testable.