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; пребацивање између online и offline режима без промене логике екрана. Заједница Android препоручује Repository као обавезан слој у Clean Architecture.
iOS имплементација Repository-ја се заснива на Swift протоколима. Протокол Repository декларише методе за добијање и чување података. Стварна имплементација се убризгава кроз иницијализатор — ово омогућава замену имплементације у тестовима и SwiftUI preview-ју. 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 — једноставне апликације са једним извором података, read-only екрани без уписа, пројекти без offline режима. У таквим случајевима, 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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође