Android Architecture Showcase: Layered Modular Architecture in Sunflower Clone

zerg 1111
9 min readDec 6, 2023

--

The Architecture Overview

This article introduces the approach to the Layered Modular Architecture, demonstrated through the Sunflower Clone project. Note: Edited in 2024/9/12.

Sub-articles :

The Benefits of Layered Modular Architecture
Implementing a Data Module in Layered Modular Architecture
Implementing a Feature Module with Android View in Layered Modular Architecture
Implementing a Feature Module with Jetpack Compose in Layered Modular Architecture

Architectural Design Overview

The Layered Modular Architecture (LMA) is designed with two primary targets in mind: to minimize development context and to achieve inherent scalability. These objectives ensure that as a project evolves, the architecture remains structured, enabling teams to iterate and scale without the need for extensive refactoring.

Modularization Strategy

This architecture adopts a modularization strategy that combines feature-based and layered approaches:

Layered Architecture:

  • Core Layer: Provides shared utilities and services used across layers without direct dependencies on other layers.
  • Domain Layer: Handles business logic, defines core operations, and is platform independent.
  • Data Layer: Manages data operations and adheres to contracts from the domain layer.
  • Feature Layer: Implements UI and feature-specific logic while staying decoupled from lower layers.
  • App Layer: Manages the overall application configuration and orchestrates dependency injection.

In conjunction with the layered architecture, LMA implements feature-based modularization to further break down functionality across each layer.

Feature-Based Modularization:

  • Modularization by feature is applied within each layer to break down functionality into distinct modules.
  • For instance, consider a scenario involving three domain objects: PlantDO, PhotoDO, and GardenPlantingDO. These domain objects correspond to specific data modules, such as data:plant, data:photo, and data:garden_planting.
  • In the feature layer, modularization is further refined based on functionality, leading to modules like feature:gallery, feature:garden_planting_list, feature:navigation, feature:plant_details, and feature:plant_list.
  • This setup promotes low coupling through clear layer separation while achieving high cohesion by grouping related functionality within each module.
  • If you’re interested in another dive into the modularization strategy, you can check out the Android official guide in this video: Modularization Strategy Guide.

Internal Modules Within a Layer:

  • Internal modules are used within each layer to facilitate code sharing among the components of that layer.
  • For example, in the data layer, the logic of mapping Data Transfer Objects (DTOs) from APIs to domain objects might need to be shared across different data modules. To facilitate this, an internal module such as data:mapping_utils can be created.
  • This internal module centralizes the mapping logic, ensuring consistency and reusability without duplicating code.
  • However, this module remains confined within the layer and is not referenced by other layers to encapsulate internal implementation details, ensuring that the layer’s internal logic does not leak into higher or adjacent layers.

Strict Dependency Management for Modularity:

  • To maintain the integrity of this modularization strategy, it is essential that each module adheres to strict dependency management.
  • Modules should only depend on the layers and components they explicitly need, minimizing unnecessary dependencies.
  • For example, while a remote API could be implemented as an internal module within the data layer, it is often more beneficial to place it in a core module.
  • In doing so, the API becomes independent of the business logic and is reusable across multiple domains.

Multiple Applications Structure:

  • The app layer supports multiple app modules, allowing each to operate independently with shared components set.
  • In this project, I created multiple app modules to demonstrate the use of different navigation frameworks. Specifically, the modules are app_sunflower_clone, app_sunflower_clone_compose, and app_sunflower_clone_navigation.
  • Each of these app modules represents a different approach to navigation within the same overall project.
  • Despite the differences in navigation frameworks, these app modules share the same set of feature modules and data modules.
  • This structure allows creating specialized modules for integration tests, enabling isolated UI tests on specific features.
  • It also supports multiple app variants, such as demo vs. production, using the same feature set but configured differently for each version.

Domain-Driven Design and Dependency Inversion

In this architecture, Domain-Driven Design (DDD) and Dependency Inversion are the foundational principles.

  • Domain-Driven Design (DDD): DDD focuses on modeling the core business logic by separating it into distinct subdomains. Using domain models and repository interfaces, it isolates business rules from external concerns like data storage or infrastructure, ensuring a clear structure and separation of responsibilities.
  • Dependency Inversion: Dependency Inversion is a principle that ensures high-level modules depend on abstractions rather than concrete implementations, allowing flexibility and decoupling by injecting actual implementations at runtime.

They work together to separate business logic from implementation details, enabling adaptation to changes in the system’s data or infrastructure. Below are the interactions between layers in relation to the principles:

  • Core Layer: Provides the infrastructures which are needed to be injected.
  • Domain Layer: Provides the core business logic through repository interfaces.
  • Data Layer: Implements repository interfaces defined in the domain layer.
  • Feature Layer: Applies business logic in the user interface using repository interfaces, domain models, and use cases.
  • App Layer: Manages dependency injection and ensures the correct repository implementations are provided at runtime.

The Implementation

Dependency Graph of Sunflower Clone.

Core Layer

Core Module App Database in Sunflower Clone

The core layer does not depend on any layer, allowing it to host shared code outside the business domain, while all other layers except the domain layer rely on the core layer. Below are the key elements:

  • Common Infrastructures: Shared setups like database configurations, network clients, and API protocols that multiple modules rely on.
  • Common Utilities: General-purpose tools, extension functions, and reusable UI components that promote code consistency and reduce redundancy across the applications.

Allowed Dependencies:

  • Core Modules: Can depend on other core modules freely for shared utilities.

Disallowed Dependencies:

  • Domain Modules: The core layer must remain independent of business logic.
  • Data Modules: It should avoid any dependency on data-handling implementations.
  • Feature Modules: The core layer should not be tied to specific features or UI elements.
  • App Modules: It must stay free from application-specific configurations to remain universally applicable.

Domain Layer

Domain Module in Sunflower Clone

The domain layer defines the core business logic, encapsulating essential rules and operations that drive the applications. It remains independent of external frameworks, focusing purely on business needs. The key components include:

  • Business Logic: Core rules and operations that define how business processes should be executed.
  • Repository Interfaces: Abstract interfaces that define the contracts for data operations. These interfaces are implemented by the data layer, ensuring the domain layer remains independent of data handling specifics.
  • Domain Models: Key entities and value objects that represent business concepts. These models are consistent across different features and encapsulate business rules.
  • Use Cases: Encapsulated business operations that execute specific tasks. Use cases are optional and can be used similarly to repositories, depending on the complexity of the domain logic. In simpler scenarios, the repository itself might be sufficient to handle the business logic without needing separate use cases.

Allowed Dependencies:

  • Domain Modules: To share and reuse domain-specific logic.

Disallowed Dependencies:

  • Core Modules: For business logic execution, the domain layer only interacts with contracts and abstractions, ensuring that it remains independent of platform-specific modules.
  • Data Modules: The domain layer should remain independent of data handling implementations.
  • Feature Modules: It should not be coupled to specific UI or feature logic.
  • App Modules: The domain layer must remain independent of application-specific configurations.

Data Layer

Data Module in Sunflower Clone

The data layer depends on the domain layer to access the contracts and interfaces it needs to implement. This allows for flexibility in swapping out data sources or modifying implementations without impacting other layers. It interacts with various data sources, such as databases, network services, and caches. The key components include:

  • Repository Implementations: Provides concrete implementations for the repository interfaces specified in the domain layer. Each repository manages a specific domain object and ensures that data operations align with business rules.
  • Data Sources: Manages different data sources, including remote APIs, local databases, and caches. It abstracts data access details, allowing other layers to focus on business logic without needing to manage how data is retrieved or stored.
  • Data Mapping: Handles the conversion of data between different representations, such as mapping database entities or API responses into domain models and vice versa. This mapping ensures that data is accurately transformed as it flows through the layers.

Allowed Dependencies:

  • Domain Modules: The data layer depends on repository interfaces to ensure that data operations conform to business logic.
  • Core Modules: Leverages shared utilities and components for database access, network clients, and other common functionalities.
  • Data Modules: Can reference other data modules when a repository requires additional data sources. However, these references should be managed carefully to avoid tight coupling between data modules.

Disallowed Dependencies:

  • Feature Modules: The data layer should not be coupled with UI or feature-specific logic.
  • App Modules: It should remain independent of application-specific configurations, ensuring consistent and reusable data handling.

Feature Layer

Feature Module Plant List in Sunflower Clone

The feature layer depends on the domain layer to access data via repository interfaces, with concrete data implementations supplied by the app layer. This setup ensures that feature modules remain decoupled from specific data implementations. Use cases, where needed, coordinate domain logic, but the feature layer can directly use repository interfaces for simpler scenarios.

The feature layer is responsible for implementing project-specific features and can contain multiple feature modules. Each module encapsulates all the frontend elements necessary for a single feature. The key components include:

  • UI Components: Defines screen elements, such as composable and fragments, that handle user interactions and display content.
  • View Models: Manages feature-specific state and business logic, ensuring a clean separation between UI and logic.
  • Layouts: Uses XML layouts or composable to define how the UI is presented, allowing UI components to control their own representations.

Allowed Dependencies:

  • Domain Modules: Relies on the domain layer to access repository interfaces and domain models, ensuring consistent application of business logic.
  • Core Modules: Utilizes shared utilities and common UI components from the core layer to maintain consistency across features.
  • Feature Modules: Can depend on other feature modules for nested scenarios or shared utilities, but should avoid unnecessary dependencies to preserve reusability.

Disallowed Dependencies:

  • Data Modules: Does not directly depend on data modules, instead relying on the domain layer.
  • App Modules: Remains independent of application-specific configurations to ensure reusability across different apps.

App Layer

App Module in Sunflower Clone

The app layer relies on the data layer for injecting concrete data implementations while integrating with the feature layer to manage and coordinate screen navigation. The app modules serve as the entry points for each application, managing essential configurations, settings, and integrations. The key components include:

  • Dependency Injection Setup: The app layer configures and provides runtime dependencies for all modules, ensuring that each app module supports dependencies from included features, data modules, and core components.
  • Navigation Management: The app layer centralizes navigation logic between features, coordinating user flows and screen transitions.
  • App Configuration: This layer manages app-specific settings, initialization logic, and global configurations required for each application.

Allowed Dependencies:

  • Shared Modules within the App Layer: Ensures that foundational components are consistent and centralized.
  • Domain Modules: Accesses business logic and repository interfaces.
  • Data Modules: Injects data implementations needed by the application.
  • Feature Modules: Manages and coordinates navigation between features.
  • Core Modules: Provides shared utilities and common components.

Disallowed Dependencies:

  • App Modules: Maintaining independence and modularity across different applications.

Conclusion

Leveraging both layered and feature-based modularization, this structured approach fosters collaboration, accelerates development cycles, and improves code quality. It supports agile iteration and responsiveness to evolving user requirements.

In my experience, adhering to these architectural principles has significantly improved the maintainability and scalability of the projects I’ve worked on. By implementing a clear separation of concerns and using dependency inversion, I have been able to isolate and address issues more efficiently. The use of feature-based modularization has allowed my team to work on different parts of the application concurrently without conflicts, speeding up development cycles. Additionally, the context required for developing each feature is significantly reduced, making it easier for developers to focus on specific tasks without being overwhelmed by the complexity of the entire application.

In summary, this architecture provides a robust foundation for developing applications that are easy to maintain and scale. By adhering to the modularization strategy, dependency inversion principles, developers can create applications that are adaptable to changing requirements, improving the overall development process and resulting in high-quality software. This approach not only enhances software quality but also improves the efficiency and effectiveness of the development process.

--

--

zerg 1111

A senior Android developer specializing in application architecture, with a focus on creating scalable and robust solutions.