Repository Pattern — паттерн, который добавляет слой абстракции между бизнес-логикой и источниками данных. Вместо прямых вызовов API, базы или кэша Repository предоставляет единый интерфейс для получения и сохранения данных. Это упрощает тестирование и переключение между источниками. Подробнее — в документации Android Data Layer.
Главное
Repository Pattern — структурный паттерн, изолирующий бизнес-логику от прямого доступа к источникам данных. Вместо того чтобы Activity, UIViewController или ViewModel напрямую вызывали Retrofit, URLSession, Room или CoreData, они обращаются к Repository. Repository решает, откуда брать данные: из сети, базы или кэша, и возвращает результат в едином формате. Это реализация принципа единственной ответственности — UI не знает, как и откуда получены данные.
Компоненты Repository включают интерфейс (protocol), реализацию и один или несколько DataSource. DataSource — класс, работающий с одним источником: RemoteDataSource вызывает API через HTTP-клиент, LocalDataSource читает и пишет в базу данных. Repository принимает DataSource через конструктор (Dependency Injection) и выбирает, к какому источнику обратиться. Например, при запросе списка пользователей Repository сначала проверяет кэш, потом базу, затем сеть.
Преимущества Repository Pattern: изоляция изменений источников данных (смена API, миграция БД) не затрагивает UI-слой; unit-тестирование через подмену Repository или DataSource; кэширование прозрачно для UI; переключение между онлайн и офлайн режимами без изменения логики экрана. Сообщество Android рекомендует Repository как обязательный слой в Clean Architecture.
iOS-реализация Repository строится на протоколах Swift. Протокол Repository объявляет методы для получения и сохранения данных. Реальная реализация внедряется через инициализатор — это позволяет подменять реализацию в тестах и preview SwiftUI. DataSource также объявляются протоколами: Protocol RemoteDataSource, Protocol LocalDataSource. ViewModel или Interactor не знает о конкретной реализации — только о протоколе Repository.
protocol UserRepository {
func getUsers() async throws -> [User]
}
protocol UserRemoteDataSource {
func fetchUsers() async throws -> [User]
}
protocol UserLocalDataSource {
func getCachedUsers() throws -> [User]
func saveUsers(_: [User]) throws
}
final class UserRepositoryImpl: UserRepository {
private let remote: UserRemoteDataSource
private let local: UserLocalDataSource
init(remote: UserRemoteDataSource, local: UserLocalDataSource) {
self.remote = remote
self.local = local
}
func getUsers() async throws -> [User] {
if let cached = try? local.getCachedUsers() {
return cached
}
let users = try await remote.fetchUsers()
try local.saveUsers(users)
return users
}
}
Dependency Injection в iOS для Repository обычно настраивается через фабрику или DI-контейнер (Swinject, Factory). В тестах протокол UserRepository заменяется mock-реализацией, возвращающей предопределённые данные. Async-await делает код синхронным и читаемым без замыканий и делегатов. Для Combine-реактивности методы Repository возвращают AnyPublisher вместо async throws.
Android-реализация Repository широко использует Kotlin Coroutines и Flow для асинхронной работы. Google рекомендует Repository в официальном гайде по архитектуре Android (Android Architecture Components). Repository принимает RemoteDataSource (Retrofit) и LocalDataSource (Room) через конструктор, а ViewModel подписывается на Flow из Repository. Repository управляет стратегией данных: сначала кэш, потом сеть или всегда сеть с записью в кэш.
interface UserRepository {
fun getUsers(): Flow<Result<List<User>>>
}
interface UserRemoteDataSource {
suspend fun fetchUsers(): List<User>
}
interface UserLocalDataSource {
fun getCachedUsers(): Flow<List<User>>
suspend fun saveUsers(users: List<User>)
}
class UserRepositoryImpl(
private val remote: UserRemoteDataSource,
private val local: UserLocalDataSource
) : UserRepository {
override fun getUsers(): Flow<Result<List<User>>> = flow {
emit(Result.Loading)
local.getCachedUsers().collect { cached ->
if (cached.isNotEmpty()) {
emit(Result.Success(cached))
}
}
try {
val users = remote.fetchUsers()
local.saveUsers(users)
emit(Result.Success(users))
} catch (e: Exception) {
emit(Result.Error(e))
}
}
}
Result-обёртка в примере выше стандартна для Android: sealed class Result сообщает ViewModel о состоянии загрузки (Loading, Success, Error). ViewModel подписывается через collect и обновляет StateFlow или LiveData. Repository с Flow автоматически уведомляет UI об изменениях в базе — это ключевое отличие от однократных запросов, где UI не узнаёт об изменениях без ручного обновления.
DataSource — классы, отвечающие за работу с конкретным источником данных. RemoteDataSource использует HTTP-клиент (URLSession, Retrofit, Ktor) для получения данных из API. LocalDataSource работает с локальным хранилищем (CoreData, Realm, Room, UserDefaults, DataStore). Каждый DataSource имеет узкую ответственность: RemoteDataSource знает только о формате API-запроса, LocalDataSource — о схеме базы данных. Repository комбинирует их, реализуя стратегию кэширования.
| DataSource | Платформа iOS | Платформа Android | Источник |
|---|---|---|---|
| Remote | URLSession + Codable | Retrofit + Moshi/Gson | REST / GraphQL API |
| Local (БД) | CoreData, SwiftData | Room, SQLDelight | SQLite на устройстве |
| Local (кэш) | NSCache, UserDefaults | DataStore, EncryptedSP | In-memory / disk |
| Preference | UserDefaults, Keychain | SharedPreferences, EncryptedSP | Настройки, токены |
Стратегии кэширования в Repository: Cache-First (сначала кэш, потом фоновая загрузка), Network-Only (только сеть, для платёжных экранов), Network-First-With-Cache-Backup (сначала сеть, при ошибке — кэш). Выбор стратегии зависит от сценария: список стран можно кэшировать надолго, курс валют — на 15 минут, баланс кошелька — только из сети. Repository реализует стратегию и меняет её без изменения ViewModel или UI.
Repository и Service — разные паттерны с пересекающимися функциями. Repository отвечает за доступ к данным и их кэширование, возвращая модели данных. Service (или Interactor, Use Case) содержит бизнес-логику: валидацию, трансформацию данных, оркестрацию вызовов нескольких Repository. Service может комбинировать UserRepository, OrderRepository и NotificationRepository для оформления заказа. Repository не содержит бизнес-логики — только CRUD и кэширование.
Когда выбирать Repository — навигация по данным с множеством источников (API + БД + кэш), offline-first архитектура, необходимость кэширования и прозрачного переключения источников. Repository обязателен в Clean Architecture и рекомендуется Google для Android-приложений. В архитектуре VIPER на iOS роль Repository выполняет Interactor-слой, взаимодействующий с Manager или Service для доступа к данным.
Когда достаточно Service — простые приложения с одним источником данных, read-only экраны без записи, проекты без офлайн-режима. В таких случаях DataSource используется напрямую ViewModel или Presenter, а Repository становится избыточным слоем. Однако добавление Repository на раннем этапе не требует больших затрат и упрощает добавление кэширования и тестов в будущем.
Часто задаваемые вопросы
DataSource — класс, работающий с одним источником (API, БД, кэш). Repository — класс, управляющий несколькими DataSource и предоставляющий единый интерфейс. Repository решает, из какого DataSource брать данные, и координирует кэширование. DataSource не знает о существовании других источников, Repository не знает деталей реализации каждого источника.
Да, Repository полезен в SwiftUI для отделения данных от View. ViewModel подписывается на Publisher из Repository, а Repository управляет кэшированием и синхронизацией. В простых приложениях можно использовать URLSession напрямую в ViewModel, но для тестируемости и масштабирования Repository предпочтительнее. Apple не навязывает паттерн, но он совместим с SwiftData и Network.framework.
DataSource заменяются mock-объектами через Dependency Injection. Тест создаёт mock RemoteDataSource (возвращает предопределённый JSON) и mock LocalDataSource (проверяет, что данные сохранены). Repository тестируется изолированно: проверяется стратегия кэширования, обработка ошибок и правильный порядок вызовов. Для интеграционных тестов используется TestDispatcher (Kotlin) или MainActor.run (Swift).
Можно, но не рекомендуется. Без протокола невозможно подменить реализацию в тестах и при preview. В Kotlin интерфейс Repository позволяет заменить реализацию через DI (Dagger, Hilt, Koin). В Swift протокол Repository обязателен для тестирования async-await и Combine-кода. Исключение — простые проекты с единственным источником данных, где Repository не несёт логики кэширования.
Offline-first — стратегия, при которой приложение работает без интернета, используя локальные данные. Repository играет ключевую роль: он сначала возвращает данные из локального DataSource, а затем синхронизируется с сервером в фоне. Пользователь видит данные мгновенно, а Repository обновляет их после загрузки из сети. Room с Flow обеспечивает реактивное обновление UI при изменении данных в локальной базе.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также