← Назад к вопросам

Как правильно организовать слои Clean Architecture для экрана с API-запросом и отображением списка

3.0 Senior🔥 142 комментариев
#Архитектура и паттерны#Работа с данными

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Организация Clean Architecture для экрана со списком из API

При организации Clean Architecture для экрана с API-запросом и списком, мы строго разделяем ответственность между слоями. Вот структура, которую я рекомендую после многолетнего опыта:

Структура слоев (от внешнего к внутреннему)

Presentation Layer (UI)
   ↓
Domain Layer (Бизнес-логика)
   ↓
Data Layer (Данные и API)

Подробная реализация каждого слоя

1. Domain Layer — ядро приложения

Здесь находятся Use Cases (Interactors) и Entities — чистые классы без Android-зависимостей.

// Entity - бизнес-сущность
data class Product(
    val id: String,
    val name: String,
    val price: Double,
    val category: String
)

// Repository интерфейс (правило Dependency Inversion)
interface ProductsRepository {
    suspend fun getProducts(): List<Product>
}

// Use Case (бизнес-правило)
class GetProductsUseCase(
    private val repository: ProductsRepository
) {
    suspend operator fun invoke(): List<Product> {
        return repository.getProducts()
    }
}

2. Data Layer — реализация источников данных

Содержит реализацию репозиториев, DTO, мапперы и источники данных (API, БД).

// DTO для API ответа
data class ProductDto(
    @SerializedName("id") val id: String,
    @SerializedName("product_name") val productName: String,
    @SerializedName("product_price") val price: Double,
    @SerializedName("product_category") val category: String
)

// Mapper для преобразования DTO -> Entity
class ProductMapper {
    fun mapToDomain(productDto: ProductDto): Product {
        return Product(
            id = productDto.id,
            name = productDto.productName,
            price = productDto.price,
            category = productDto.category
        )
    }
}

// Реализация Repository
class ProductsRepositoryImpl(
    private val apiService: ProductsApiService,
    private val mapper: ProductMapper
) : ProductsRepository {
    override suspend fun getProducts(): List<Product> {
        val response = apiService.getProducts()
        return response.map { mapper.mapToDomain(it) }
    }
}

3. Presentation Layer — UI и ViewModels

Используем MVVM или MVI вместе с Clean Architecture.

// ViewModel с состоянием
class ProductsViewModel(
    private val getProductsUseCase: GetProductsUseCase
) : ViewModel() {
    
    private val _state = MutableStateFlow<ProductsState>(ProductsState.Loading)
    val state: StateFlow<ProductsState> = _state.asStateFlow()
    
    fun loadProducts() {
        viewModelScope.launch {
            _state.value = ProductsState.Loading
            try {
                val products = getProductsUseCase()
                _state.value = ProductsState.Success(products)
            } catch (e: Exception) {
                _state.value = ProductsState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

// Состояния UI
sealed class ProductsState {
    object Loading : ProductsState()
    data class Success(val products: List<Product>) : ProductsState()
    data class Error(val message: String) : ProductsState()
}

Критические принципы организации

  1. Односторонняя зависимость:

    • Presentation → Domain ← Data
    • Domain слой ничего не знает о других слоях
  2. Dependency Injection:

    // Модуль в Dagger/Hilt
    @Module
    @InstallIn(SingletonComponent::class)
    class AppModule {
        @Provides
        fun provideProductsRepository(
            apiService: ProductsApiService
        ): ProductsRepository {
            return ProductsRepositoryImpl(apiService, ProductMapper())
        }
        
        @Provides
        fun provideGetProductsUseCase(
            repository: ProductsRepository
        ): GetProductsUseCase {
            return GetProductsUseCase(repository)
        }
    }
    
  3. Тестируемость:

    // Тест Use Case
    class GetProductsUseCaseTest {
        private lateinit var useCase: GetProductsUseCase
        private val mockRepository = mock<ProductsRepository>()
        
        @Test
        fun `invoke should return products from repository`() = runTest {
            val expectedProducts = listOf(Product("1", "Test", 10.0, "Category"))
            whenever(mockRepository.getProducts()).thenReturn(expectedProducts)
            
            useCase = GetProductsUseCase(mockRepository)
            val result = useCase()
            
            assertEquals(expectedProducts, result)
        }
    }
    

Практические советы по реализации

  • Корумтные состояния: Всегда обрабатывайте loading, success, error состояния
  • Пагинация: Для списков добавьте пагинацию в UseCase и Repository
  • Кэширование: В Data Layer добавьте стратегию "Network-first with cache fallback"
  • Мультиплатформенность: Domain слой можно использовать в Kotlin Multiplatform

Распространенные ошибки

  • Смешивание DTO и Entity объектов
  • Помещение Android-зависимостей в Domain слой
  • Отсутствие правильного управления состоянием в Presentation Layer
  • Игнорирование обработки ошибок на всех уровнях

Такая организация гарантирует тестируемость, поддерживаемость и гибкость при изменении требований. Каждый слой имеет одну ответственность и может изменяться независимо от других.

Как правильно организовать слои Clean Architecture для экрана с API-запросом и отображением списка | PrepBro