← Назад к вопросам
Как правильно организовать слои 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()
}
Критические принципы организации
-
Односторонняя зависимость:
- Presentation → Domain ← Data
- Domain слой ничего не знает о других слоях
-
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) } } -
Тестируемость:
// Тест 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
- Игнорирование обработки ошибок на всех уровнях
Такая организация гарантирует тестируемость, поддерживаемость и гибкость при изменении требований. Каждый слой имеет одну ответственность и может изменяться независимо от других.