Modularization and Project Optimization in Android Development

Building Scalable, Maintainable, and High-Performance Android Apps with Clean Architecture and Best Practices.

Mohamed Aymen MEJRI
7 min readFeb 8, 2025
Photo by Matt Briney on Unsplash

As Android applications become more complex, developers often struggle with scalability, maintainability, and performance bottlenecks. A poorly structured project can lead to longer build times, slower app performance, and difficulties in managing features.

To address these challenges, modern Android development relies on two key strategies:

  1. Modularization — Splitting a large project into independent modules to improve maintainability, enhance reusability, and reduce build times.
  2. Project Optimization — Implementing best practices and utilizing efficient libraries to reduce app size, improve load times, optimize networking, and enhance overall performance.

This article provides a detailed guide to modularization and project optimization, covering best practices, real-world implementation, and modern libraries like Coil for image loading, Ktor for networking, and Koin for dependency injection.

1. Modularization in Android Projects

In a traditional monolithic Android project, all code resides within a single module (app). While this setup works for small applications, it becomes a maintenance nightmare as the codebase expands. Modularization solves this by splitting the project into multiple modules, each handling specific functionality.

Key Benefits of Modularization

  • Faster Build Times — When working on a monolithic project, even a small code change requires rebuilding the entire application. With modularization, only the affected module needs to be rebuilt, leading to significantly faster build times.
  • Better Code Organization — Separating features and business logic into different modules makes it easier to maintain and navigate the project.
  • Reusability — Common components (e.g., authentication, database handling) can be reused across multiple projects.
  • Independent Feature Development — Teams can work on different modules simultaneously without affecting each other.
  • Scalability — Modular projects are easier to scale by adding new features without affecting existing functionality.

Types of Modules in an Android Project (Following Clean Architecture & SOLID Principles)

A properly structured Android project should be divided into three main layers, following Clean Architecture principles:

  1. Presentation Layer (Feature Modules) — Handles UI, user interactions, and state management. It is divided into multiple feature modules, where each module represents an independent app feature (e.g., feature_home, feature_profile).
  2. Domain Layer — Contains business logic, use cases, and repository interfaces. It is independent of the Android framework, making it highly testable.
  3. Data Layer — Handles data sources (API calls, databases, and caching). It provides implementations of repository interfaces defined in the domain layer.

Feature-Based Modularization for the Presentation Layer

Instead of a single, large presentation module, we split it into multiple feature modules to:

  1. Improve maintainability and independent feature development.
  2. Reduce build times by compiling only affected modules.
  3. Allow dynamic feature delivery (on-demand loading).

Example of a Modularized Project Structure

app/  
├── feature_home/ # Home Feature
│ ├── ui/ # Jetpack Compose UI Components
│ ├── viewmodel/ # ViewModel for Home Feature
│ ├── di/ # Dependency Injection for Home
├── feature_profile/ # Profile Feature
│ ├── ui/ # Jetpack Compose UI Components
│ ├── viewmodel/ # ViewModel for Profile Feature
│ ├── di/ # Dependency Injection for Profile
├── feature_settings/ # Settings Feature
│ ├── ui/ # Jetpack Compose UI Components
│ ├── viewmodel/ # ViewModel for Settings Feature
│ ├── di/ # Dependency Injection for Settings
├── domain/
│ ├── models/ # Entities & Domain Models
│ ├── usecases/ # Business Logic (Use Cases)
│ ├── repository/ # Repository Interfaces
├── data/
│ ├── network/ # API Calls using Ktor
│ ├── database/ # Room Database Implementation
│ ├── repository/ # Repository Implementations
│ ├── preferences/ # DataStore for Shared Preferences
│ ├── di/ # DI Modules for Data Layer
├── core/ # Common Utilities & Base Classes
│ ├── utils/ # Extension Functions & Helpers
│ ├── di/ # Core Dependency Injection Modules
│ ├── constants/ # App-Wide Constants
│ ├── base/ # Base Classes for ViewModels & Use Cases or activities

How This Structure Aligns with Clean Architecture

  1. Presentation Layer (feature_* modules).
    - Divided into multiple feature modules, each representing a distinct UI feature.
    - Uses Jetpack Compose for UI and ViewModel for UI state management.
    - Uses dependency injection (Hilt or Koin) to provide dependencies locally.
  2. Domain Layer (domain/)
    - Contains business logic and use cases that execute specific operations.
    - Defines repository interfaces that do not depend on any framework (fully testable).
  3. Data Layer (data/)
    - Implements repository interfaces to fetch and store data.
    - Uses Ktor for networking, Room for local storage, and DataStore for preferences.

Applying SOLID Principles in Modularization

+----------------+-------------------------------------------+------------------------------------------------+
| Principle | How It Applies to Modules | Code Example |
+----------------+-------------------------------------------+------------------------------------------------+
| SRP | Each module (feature, domain, data) | `UserRepository` handles only data, |
| (Single | has a single responsibility. | ViewModels manage UI logic separately. |
| Responsibility)| | |
+----------------+-------------------------------------------+------------------------------------------------+
| OCP | Core modules (`core/`) allow extension | `BaseViewModel` can be extended in |
| (Open-Closed) | without modifying existing code. | different features without modification. |
+----------------+-------------------------------------------+------------------------------------------------+
| LSP | Data sources (API, local DB) should be | `UserRepository` can switch between |
| (Liskov | interchangeable without modifying | **remote API** and **local DB** without |
| Substitution) | dependent code. | affecting the domain layer. |
+----------------+-------------------------------------------+------------------------------------------------+
| ISP | Modules depend only on necessary parts | `ReadableUser` and `WritableUser` interfaces |
| (Interface | of shared interfaces, avoiding large, | separate read and write operations instead of |
| Segregation) | monolithic interfaces. | a single large `UserRepository` interface. |
+----------------+-------------------------------------------+------------------------------------------------+
| DIP | The domain layer depends on | `UserUseCase` depends on `UserRepository` |
| (Dependency | abstractions, not implementations. | (interface), injected using **Koin** or **Hilt**. |
| Inversion) | | This ensures flexibility and testability. |
+----------------+-------------------------------------------+------------------------------------------------+

Dependency Flow in Clean Architecture with Feature-Based Presentation Layer

[Feature Modules]  →  [Domain Layer]   →   [Data Layer]
(UI & ViewModel) (Use Cases) (Repositories)
  • Each feature module (e.g., feature_home, feature_profile) interacts with the domain layer but does not depend on other features.
  • The domain layer provides a clear API for each feature by exposing use cases and repository interfaces.
  • The data layer implements repository interfaces and handles data fetching/storage.

Key Benefits of This Modularized Approach

  • Scalability — New features can be added as separate modules without affecting existing code.
  • Faster Build Times — Changes in one feature module do not require rebuilding the entire app.
  • Independent Feature Development — Teams can work on different modules simultaneously.
  • Improved Code Reusability — Shared logic (e.g., authentication, database) is centralized in core/domain modules.
  • Dynamic Feature Delivery — Feature modules can be loaded on demand using Play Feature Delivery.

How to Implement Modularization in Android?

1. Create a New Module in Android Studio

Go to File > New > New Module, select Android Library or Feature Module, and configure the module.

2. Define Dependencies in build.gradle

Each module should include only the necessary dependencies to reduce build times and avoid unnecessary complexity. Example:

dependencies {
implementation project(":core")
implementation project(":ui")
}

Dependency Injection (DI) in Modularized Projects

Since modules should be loosely coupled, dependency injection (DI) helps manage dependencies efficiently. There are two popular DI frameworks in Android: Dagger/Hilt and Koin.

Using Hilt for Dependency Injection

Hilt is a DI framework built on top of Dagger. It simplifies dependency injection and provides excellent support for modularization.

@InstallIn(SingletonComponent::class)
@Module
object NetworkModule {
@Provides
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}

Using Koin for Dependency Injection

Koin is a lightweight DI framework that uses Kotlin DSL for defining dependencies. It’s simpler than Hilt and does not require annotations or code generation.

val networkModule = module {
single {
Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}

In a modularized project, each module can define its own Koin module and load dependencies dynamically.

2. Project Optimization Techniques for Better Performance

Optimizing an Android project ensures that the application runs smoothly, loads faster, and consumes fewer resources.

2.1 Optimize Image Loading with Coil in Jetpack Compose

Why Use Coil?

  • Fast and lightweight — Designed for Android with minimal overhead.
  • Built-in caching — Uses memory and disk caching to reduce network requests.
  • Seamless Jetpack Compose integration — Supports AsyncImage, making it easy to load images efficiently.

Loading Images with Coil in Jetpack Compose :

import coil.compose.AsyncImage

@Composable
fun UserProfileImage(imageUrl: String) {
AsyncImage(
model = imageUrl,
contentDescription = "Profile Image",
placeholder = painterResource(R.drawable.placeholder),
error = painterResource(R.drawable.error),
contentScale = ContentScale.Crop,
modifier = Modifier.size(128.dp).clip(CircleShape)
)
}

Coil Caching for Performance Optimization :

val imageLoader = ImageLoader.Builder(context)
.crossfade(true)
.diskCachePolicy(CachePolicy.ENABLED)
.memoryCachePolicy(CachePolicy.ENABLED)
.build()

2.2 Efficient Networking with Ktor

Why Use Ktor?

  • Asynchronous and non-blocking — Improves efficiency in network operations.
  • Lightweight and flexible — Reduces memory consumption compared to Retrofit.
  • Full Kotlin support — Works seamlessly with Kotlin Coroutines.

Setting Up Ktor in Android :

dependencies {
implementation("io.ktor:ktor-client-core:x.y.z")
implementation("io.ktor:ktor-client-serialization:x.y.z")
}

Using Ktor to Fetch Data :

import io.ktor.client.*
import io.ktor.client.request.*
import kotlinx.coroutines.*

suspend fun fetchUser(userId: Int): User {
return client.get("https://api.example.com/users/$userId")
}

Using Ktor in Jetpack Compose :

@Composable
fun UserProfile(userId: Int) {
var user by remember { mutableStateOf<User?>(null) }

LaunchedEffect(userId) {
user = fetchUser(userId)
}

user?.let {
Text(text = "Name: ${it.name}")
Text(text = "Email: ${it.email}")
} ?: CircularProgressIndicator()
}

Conclusion

By implementing modularization and optimizing project performance, Android developers can build scalable, maintainable, and high-performance applications.

Key Takeaways:

Modularization improves project structure, build times, and scalability.
✔ Use Hilt or Koin for better dependency management in a modularized project.
✔ Optimize image loading with Coil for faster UI performance.
✔ Use Ktor for networking to improve efficiency and reduce resource usage.
Reduce APK size and improve app startup time using Jetpack’s optimization libraries.

Following these best practices ensures that your Android app remains efficient, scalable, and user-friendly.

--

--

Mohamed Aymen MEJRI
Mohamed Aymen MEJRI

Written by Mohamed Aymen MEJRI

Lead Android Developer @Hager Electro SAS

No responses yet