Skip to main content

Introduction

TecMeli is built following Clean Architecture principles combined with the MVVM (Model-View-ViewModel) pattern. This architecture ensures separation of concerns, testability, and maintainability across the codebase.

Architecture Diagram

Layer Overview

TecMeli follows a three-layer architecture:

UI Layer

Jetpack Compose screens, ViewModels, and UI state management

Domain Layer

Business logic, use cases, and domain models

Data Layer

Data sources, repositories, and DTOs

UI Layer

Location: ui/ package Responsibilities:
  • Render UI using Jetpack Compose
  • Observe ViewModel state changes
  • Handle user interactions
  • Display loading, success, and error states
Key Components:
  • Screens: Composable functions that define the UI (HomeScreen, ProductDetailScreen)
  • ViewModels: Manage UI state and business logic orchestration
  • UI State: Sealed class representing different states (UiState.kt:11)
Example:
@HiltViewModel
class HomeViewModel @Inject constructor(
    private val getProductsUseCase: GetProductsUseCase
) : ViewModel() {
    private val _uiState = MutableStateFlow<UiState<List<Product>>>(UiState.Empty)
    val uiState: StateFlow<UiState<List<Product>>> = _uiState.asStateFlow()
    
    fun searchProducts(query: String) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            getProductsUseCase(query)
                .onSuccess { products -> _uiState.value = UiState.Success(products) }
                .onFailure { error -> _uiState.value = UiState.Error(...) }
        }
    }
}

Domain Layer

Location: domain/ package Responsibilities:
  • Define business logic through use cases
  • Declare repository contracts (interfaces)
  • Define domain models (pure Kotlin data classes)
  • Remain independent of frameworks and external libraries
Key Components:
  • Use Cases: Encapsulate single business operations (GetProductsUseCase, GetProductDetailUseCase)
  • Repository Interfaces: Define data access contracts (ProductRepository, TokenRepository)
  • Domain Models: Business entities (Product, ProductDetail, ProductAttribute)
Example:
class GetProductsUseCase @Inject constructor(
    private val repository: ProductRepository
) {
    suspend operator fun invoke(query: String): Result<List<Product>> {
        return if (query.isBlank()) {
            Result.success(emptyList())
        } else {
            repository.searchProducts(query)
        }
    }
}

Data Layer

Location: data/ package Responsibilities:
  • Implement repository interfaces
  • Fetch data from remote APIs
  • Map DTOs to domain models
  • Handle network errors and responses
Key Components:
  • Repository Implementations: Concrete repository classes (ProductRepositoryImpl, TokenRepositoryImpl)
  • Remote APIs: Retrofit interfaces (MeliApi, AuthApi)
  • DTOs: Data Transfer Objects for network responses (ResultsDto, SearchResponseDto)
  • Mappers: Extension functions to convert DTOs to domain models (ProductMapper.kt)
Example:
class ProductRepositoryImpl @Inject constructor(
    private val meliApi: MeliApi
) : ProductRepository {
    override suspend fun searchProducts(query: String): Result<List<Product>> = safeApiCall(
        call = { meliApi.searchProducts(query = query) },
        transform = { body -> body.results.map { it.toDomain() } }
    )
}

Package Structure

com.alcalist.tecmeli/
├── core/
│   ├── di/              # Dependency injection modules
│   ├── network/         # Network utilities and error handling
│   └── util/            # Common utilities
├── data/
│   ├── mapper/          # DTO to domain model mappers
│   ├── remote/
│   │   ├── api/         # Retrofit API interfaces
│   │   └── dto/         # Data Transfer Objects
│   └── repository/      # Repository implementations
├── domain/
│   ├── model/           # Domain models
│   ├── repository/      # Repository interfaces
│   └── usecase/         # Business logic use cases
└── ui/
    ├── components/      # Reusable UI components
    ├── navigation/      # Navigation setup
    ├── screen/          # Feature screens
    └── theme/           # Material 3 theming

Data Flow

1

User Interaction

User interacts with a Composable screen (e.g., searches for a product)
2

ViewModel Invocation

Screen calls a ViewModel method (e.g., viewModel.searchProducts(query))
3

Use Case Execution

ViewModel invokes the appropriate use case with business logic
4

Repository Call

Use case calls the repository interface method
5

Data Fetching

Repository implementation fetches data from remote API via Retrofit
6

Mapping

DTOs are mapped to domain models using mapper functions
7

Result Propagation

Result flows back through use case to ViewModel
8

State Update

ViewModel updates UI state (UiState)
9

UI Recomposition

Compose UI observes state change and recomposes

Key Benefits

Each layer has a single, well-defined responsibility. UI handles presentation, domain contains business logic, and data manages data access.
Each layer can be tested independently. Use cases can be tested without UI or network, repositories can be mocked in use case tests.
Changes in one layer don’t affect others. For example, switching from Retrofit to Ktor only affects the data layer.
Domain layer is framework-agnostic. Business logic doesn’t depend on Android, Compose, or Retrofit.
New features follow the same pattern, making the codebase predictable and easy to extend.

Technology Stack

  • UI Framework: Jetpack Compose with Material 3
  • Architecture Pattern: MVVM + Clean Architecture
  • Dependency Injection: Hilt (Dagger)
  • Networking: Retrofit + OkHttp
  • Serialization: Gson
  • Async: Kotlin Coroutines + Flow
  • Navigation: Jetpack Navigation Compose

Next Steps

Build docs developers (and LLMs) love