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 декларира методи за получаване и запазване на данни. Реалната имплементация се инжектира чрез инициализатора — това позволява замяна на имплементацията в тестове и 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 | В паметта / диск |
| Предпочитания | 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 е достатъчен — прости приложения с един източник на данни, екрани само за четене без запис, проекти без офлайн режим. В такива случаи 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).
Може, но не се препоръчва. Без протокол не може да се замени имплементацията в тестове и прегледи. В 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също