Android Application Architecture Showcase : Sunflower Clone

zerg 1111
7 min readDec 6, 2023

--

The Architecture Overview

This article introduces the approach to Android application architecture, demonstrated through the Sunflower Clone project. Note: Edited in 2024/7/7.

Sub-articles : Implementing Feature Modules with Jetpack Compose in Sunflower Clone

Architectural Design in the Sunflower Clone Project: A Comprehensive Overview

The Sunflower Clone project exemplifies advanced architectural design principles aimed at enhancing maintainability, scalability, and developer efficiency. Here’s an in-depth look at how these key architectural strategies are implemented:

Modularization Strategy

The project employs a hybrid modularization approach, integrating both layered and feature-based strategies to ensure a robust and maintainable architecture.

Layer-Based Modularization: This methodology divides the application into distinct modules, each addressing specific responsibilities to maintain a clear separation of concerns:

  • Domain Module: Encapsulates the core business logic, providing repository interfaces, domain models, and use cases.
  • Data Modules: Handle data operations and provide implementations for repository interfaces. For instance, separate modules manage different types of data (e.g., user data, product data).
  • Feature Modules: Concentrate on the UI and application functionality, organized around various application features.
  • Core Modules: Comprise independent components reusable across the application, such as database operations, network requests, and common UI components.
  • App Modules: Serve as the entry points of the application, managing dependency injection, navigation, and other essential application configurations. Multiple app modules can exist, sharing components and facilitating the development of different apps.

Feature-Based Modularization: This strategy further refines the project structure by dividing modules based on specific domain aspects within each layer. For example, within the data layer, there may be distinct modules for handling different types of data (e.g., user data, product data). Similarly, within the feature layer, modules are organized around different application features, ensuring each module addresses a specific domain aspect comprehensively.

Dependency Inversion Principle

The Dependency Inversion Principle (DIP) is integral to the project’s architecture, focusing on the abstraction of modules from specific implementations.

  • Decoupling: Feature modules interact with data through repository interfaces provided by the domain module rather than concrete implementations. This abstraction allows feature modules to remain independent of data modules. Such decoupling enables flexible substitution of different data sources or technologies without altering the feature modules, allowing each module to evolve independently and reducing the risk of cascading changes, thereby improving overall code quality.
  • Dependency Injection: Implementations of these interfaces are injected into the modules at runtime, often using frameworks like Dagger or Hilt. This approach minimizes module dependencies and promotes easier maintenance and scalability.
  • Flexibility and Extensibility: DIP supports the changes in implementation without impacting existing modules. It facilitates concurrent refactoring by allowing repository implementations to be switched independently, enabling gradual and manageable updates to the codebase.
  • Testability: By adhering to DIP, the application becomes highly testable. Fake implementations can be easily substituted for real ones during unit testing, enabling thorough testing of modules without dependencies on actual data sources.

Single Activity Pattern

The project employs Single Activity Pattern to streamline navigation and lifecycle management across the application.

  • Centralized Navigation Control: The single activity functions as the central controller for all navigation, overseeing the app’s lifecycle and navigation stack. This centralization simplifies the management of navigation flows and ensures that all navigation-related code is maintained in one location. The choice of navigation framework is flexible, accommodating various implementations.
  • Simplified Lifecycle Management: By consolidating lifecycle management within a single activity, the architecture reduces the complexity associated with managing multiple activities. This approach enhances performance and delivers a more consistent user experience, as all lifecycle-related logic is centralized.

The Implementation

Dependency graph of Sunflower Clone

App Module

App Module of Sunflower Clone

The app module serves as the entry point of the application, integrating all other modules to provide a cohesive user experience. Its key responsibilities include:

  • Activity Implementation: The app module hosts the main activity of the application, which typically serves as the entry point for user interaction and navigation.
  • Dependency Injection: The app module sets up dependency injection frameworks such as Dagger or Koin to facilitate the injection of dependencies throughout the application.
  • Navigation: It contains the navigation logic for the entire application, facilitating seamless transitions between different screens and features.

Core Module

Core Module App Database of Sunflower Clone

Core modules house shared code that is not specific to any particular domain. This includes:

  • Database Module: Manages database-related functionalities, such as database creation, migration, and access.
  • Network API Module: Handles network-related operations, including API calls, request/response parsing, and error handling.
  • UI Module: Contains reusable UI components and resources to ensure consistent UI across different features of the application. Centralizing UI theming enables easier style changes and maintenance.

Data Module

Data Module of Sunflower Clone

Data modules are responsible for providing implementations of repositories, which abstract the data sources used by the application. This includes:

  • Repository Implementation: Encapsulates the implementation details of specific repositories, such as data retrieval and manipulation logic.
  • Modularity: Enables seamless swapping of data sources by modifying build.gradle configurations, without impacting other modules.
  • Aggregated Access: Establishes dependencies on other data modules to access aggregated data. This allows for efficient retrieval and management of complex data structures, enabling the application to access and manipulate aggregated data sources seamlessly.

Domain Module

Domain Module of Sunflower Clone

The domain module is a pure Kotlin module that encapsulates the business logic of the application. It includes:

  • Domain Object Definitions: Defines domain-specific entities and value objects that represent the core concepts and data structures of the application.
  • Repository Interfaces: Specifies interfaces for repositories, defining the contract for data access and manipulation.
  • Use Cases: Implements use cases, which encapsulate application-specific business logic and orchestrate interactions between different domain objects and repositories.

Feature Module

Feature Module Plant List of Sunflower Clone

Feature modules serve as self-contained units within the application, each dedicated to specific functionalities or user interactions. They encompass:

  • Views: Represent individual screens or widgets within the application, presenting information and facilitating user interaction.
  • View Models: Manage UI-related data and state within the feature, ensuring separation of concerns and promoting reusability by encapsulating UI logic. They provide an interface for the UI to interact with the underlying data and business logic.
  • Modularity: Similar to data modules, feature modules can establish dependencies on other feature modules to create nested UI structures. This modular approach enables the construction of complex user interfaces by combining and reusing smaller, self-contained features.

Unit Testing and Code Reuse

Each module incorporates its own unit test suite, validating critical functionalities such as data mapping, repository operations, and UI logic. Examples include:

  • Mapper Test: Tests for mapping functions ensuring accurate transformation of data.
  • Repository Test: Ensures reliable data retrieval and manipulation within the application context.
  • View Model Test: Validates the behavior and state management of UI components.
  • Shared Code in Module Groups: In cases of duplicated functionality across modules (e.g., :data:photo and :data:plant), creating a dedicated shared module promotes code reuse and maintains consistency.
//MapperExt.kt in :data:photo
internal fun List<PhotoDto>.toPhotoRemoteItemsList(
page: Int,
pageSize: Int,
plantName: String
): List<PhotoRemoteItems> {
val offset = (page - 1) * pageSize

return mapIndexed { index, photo ->
with(photo) {
PhotoRemoteItems(
photoEntity = PhotoEntity(
authorId = authorId,
authorName = authorName,
imageUrl = url,
photoId = photoId,
plantName = plantName
),
photoRemoteOrderEntity = PhotoRemoteOrderEntity(
photoId = photoId,
remoteOrder = index + offset
)
)
}
}
}

internal fun PhotoItemView.toPhotoDO(attributionUrl: String) = PhotoDO(
attributionUrl = attributionUrl,
authorId = authorId,
authorName = authorName,
imageUrl = imageUrl,
photoId = photoId,
plantName = plantName
)

By isolating parts of the logic into mapper extensions, we can create unit tests as shown below.

//MapperExtTest.kt in :data:photo
class MapperExtTest {
@Test
fun mapPhotoDtoListToPhotoRemoteItemsList() {
//Given
val page = getRandomInt()
val pageSize = getRandomInt()
val photoDtoList = listOf(
PhotoDto(
authorId = getRandomStr(),
authorName = getRandomStr(),
photoId = getRandomStr(),
url = getRandomStr()
),
PhotoDto(
authorId = getRandomStr(),
authorName = getRandomStr(),
photoId = getRandomStr(),
url = getRandomStr()
),
PhotoDto(
authorId = getRandomStr(),
authorName = getRandomStr(),
photoId = getRandomStr(),
url = getRandomStr()
)
)
val plantName = getRandomStr()

//When
val photoRemoteItemsList = photoDtoList.toPhotoRemoteItemsList(
page = page,
pageSize = pageSize,
plantName = plantName
)

//Then
val offset = (page - 1) * pageSize

assert(photoRemoteItemsList.filterIndexed { index, photoRemoteItems ->
val photoDto = photoDtoList[index]

with(photoRemoteItems.photoEntity) {
authorId == photoDto.authorId
&& authorName == photoDto.authorName
&& imageUrl == photoDto.url
&& photoId == photoDto.photoId
} && with(photoRemoteItems.photoRemoteOrderEntity) {
photoId == photoDto.photoId && remoteOrder == index + offset
}
}.size == photoDtoList.size)
}

@Test
fun mapPhotoItemViewToPhotoDO() {
//Given
val attributionUrl = getRandomStr()
val photoItemView = PhotoItemView(
authorId = getRandomStr(),
authorName = getRandomStr(),
imageUrl = getRandomStr(),
photoId = getRandomStr(),
plantName = getRandomStr(),
remoteOrder = getRandomInt()
)

//When
val photoDO = photoItemView.toPhotoDO(attributionUrl = attributionUrl)

//Then
assert(photoDO.attributionUrl == attributionUrl)
assert(photoDO.authorId == photoItemView.authorId)
assert(photoDO.authorName == photoItemView.authorName)
assert(photoDO.imageUrl == photoItemView.imageUrl)
assert(photoDO.photoId == photoItemView.photoId)
assert(photoDO.plantName == photoItemView.plantName)
}

private fun getRandomInt() = Random.nextInt()
private fun getRandomStr() = UUID.randomUUID().toString()
}

Conclusion

The architectural design of the Sunflower Clone project emphasizes scalability, flexibility, and maintainability. By using a hybrid modularization strategy and key architectural principles, the project enables developers to produce efficient, adaptable code.

Leveraging both layered and feature-based modularization, the project ensures a clear separation of concerns while enhancing reusability and autonomy across components. This approach allows for seamless integration of technologies and frameworks, such as Hilt for dependency injection, and Jetpack Compose for UI development.

This structured approach fosters collaboration, accelerates development cycles, and improves code quality. It supports agile iteration and responsiveness to evolving user requirements.

In summary, the Sunflower Clone project exemplifies effective architectural practices that optimize development processes and provide a strong foundation for sustained software excellence.

https://github.com/Deathhit/SunflowerClone

--

--