Repository Pattern — wzorzec, który dodaje warstwę abstrakcji między logiką biznesową a źródłami danych. Zamiast bezpośrednich wywołań API, bazy danych lub pamięci podręcznej Repository udostępnia jednolity interfejs do pobierania i zapisywania danych. Ułatwia to testowanie i przełączanie między źródłami. Więcej w dokumentacji Android Data Layer.
Najważniejsze
Repository Pattern — wzorzec strukturalny izolujący logikę biznesową od bezpośredniego dostępu do źródeł danych. Zamiast aby Activity, UIViewController lub ViewModel bezpośrednio wywoływały Retrofit, URLSession, Room lub CoreData, odwołują się do Repository. Repository decyduje, skąd pobrać dane: z sieci, bazy danych lub pamięci podręcznej, i zwraca wynik w jednolitym formacie. Jest to realizacja zasady pojedynczej odpowiedzialności — UI nie wie, jak i skąd dane zostały pobrane.
Składniki Repository obejmują interfejs (protocol), implementację oraz jeden lub więcej DataSource. DataSource — klasa pracująca z jednym źródłem: RemoteDataSource wywołuje API przez klienta HTTP, LocalDataSource odczytuje i zapisuje do bazy danych. Repository przyjmuje DataSource przez konstruktor (Dependency Injection) i wybiera, do którego źródła się odwołać. Na przykład przy żądaniu listy użytkowników Repository najpierw sprawdza pamięć podręczną, potem bazę danych, następnie sieć.
Zalety Repository Pattern: izolacja zmian źródeł danych (zmiana API, migracja BD) nie wpływa na warstwę UI; testy jednostkowe przez podmianę Repository lub DataSource; buforowanie przejrzyste dla UI; przełączanie między trybem online i offline bez zmiany logiki ekranu. Społeczność Androida zaleca Repository jako obowiązkową warstwę w Clean Architecture.
Implementacja iOS Repository opiera się na protokołach Swift. Protokół Repository deklaruje metody pobierania i zapisywania danych. Rzeczywista implementacja jest wstrzykiwana przez inicjalizator — umożliwia to podmianę implementacji w testach i podglądzie SwiftUI. DataSource również są deklarowane przez protokoły: Protocol RemoteDataSource, Protocol LocalDataSource. ViewModel lub Interactor nie zna konkretnej implementacji — zna tylko protokół 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 w iOS dla Repository jest zwykle konfigurowana przez fabrykę lub kontener DI (Swinject, Factory). W testach protokół UserRepository jest zastępowany implementacją mock zwracającą predefiniowane dane. Async-await sprawia, że kod jest synchroniczny i czytelny bez domknięć i delegatów. Dla reaktywności Combine metody Repository zwracają AnyPublisher zamiast async throws.
Implementacja Android Repository szeroko wykorzystuje Kotlin Coroutines i Flow do pracy asynchronicznej. Google zaleca Repository w oficjalnym przewodniku po architekturze Androida (Android Architecture Components). Repository przyjmuje RemoteDataSource (Retrofit) i LocalDataSource (Room) przez konstruktor, a ViewModel subskrybuje Flow z Repository. Repository zarządza strategią danych: najpierw pamięć podręczna, potem sieć lub zawsze sieć z zapisem do pamięci podręcznej.
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))
}
}
}
Otoka Result w powyższym przykładzie jest standardem dla Androida: sealed class Result informuje ViewModel o stanie ładowania (Loading, Success, Error). ViewModel subskrybuje przez collect i aktualizuje StateFlow lub LiveData. Repository z Flow automatycznie powiadamia UI o zmianach w bazie danych — to kluczowa różnica w stosunku do jednorazowych zapytań, gdzie UI nie dowiaduje się o zmianach bez ręcznego odświeżania.
DataSource — klasy odpowiedzialne za pracę z konkretnym źródłem danych. RemoteDataSource używa klienta HTTP (URLSession, Retrofit, Ktor) do pobierania danych z API. LocalDataSource pracuje z lokalnym magazynem (CoreData, Realm, Room, UserDefaults, DataStore). Każdy DataSource ma wąską odpowiedzialność: RemoteDataSource zna tylko format żądania API, LocalDataSource — schemat bazy danych. Repository łączy je, implementując strategię buforowania.
| DataSource | Platforma iOS | Platforma Android | Źródło |
|---|---|---|---|
| Remote | URLSession + Codable | Retrofit + Moshi/Gson | REST / GraphQL API |
| Local (BD) | CoreData, SwiftData | Room, SQLDelight | SQLite na urządzeniu |
| Local (pamięć podręczna) | NSCache, UserDefaults | DataStore, EncryptedSP | In-memory / dysk |
| Preference | UserDefaults, Keychain | SharedPreferences, EncryptedSP | Ustawienia, tokeny |
Strategie buforowania w Repository: Cache-First (najpierw pamięć podręczna, potem ładowanie w tle), Network-Only (tylko sieć, dla ekranów płatności), Network-First-With-Cache-Backup (najpierw sieć, przy błędzie — pamięć podręczna). Wybór strategii zależy od scenariusza: listę krajów można buforować na długo, kursy walut — na 15 minut, saldo portfela — tylko z sieci. Repository implementuje strategię i zmienia ją bez modyfikacji ViewModel lub UI.
Repository i Service — różne wzorce o nakładających się funkcjach. Repository odpowiada za dostęp do danych i ich buforowanie, zwracając modele danych. Service (lub Interactor, Use Case) zawiera logikę biznesową: walidację, transformację danych, orkiestrację wywołań wielu Repository. Service może łączyć UserRepository, OrderRepository i NotificationRepository do realizacji zamówienia. Repository nie zawiera logiki biznesowej — tylko CRUD i buforowanie.
Kiedy wybrać Repository — nawigacja po danych z wieloma źródłami (API + BD + pamięć podręczna), architektura offline-first, potrzeba buforowania i przejrzystego przełączania źródeł. Repository jest obowiązkowe w Clean Architecture i zalecane przez Google dla aplikacji Android. W architekturze VIPER na iOS rolę Repository pełni warstwa Interactor, współpracująca z Manager lub Service w celu dostępu do danych.
Kiedy wystarczy Service — proste aplikacje z jednym źródłem danych, ekrany tylko do odczytu bez zapisu, projekty bez trybu offline. W takich przypadkach DataSource jest używany bezpośrednio przez ViewModel lub Presenter, a Repository staje się nadmiarową warstwą. Jednak dodanie Repository na wczesnym etapie nie wymaga dużych nakładów i ułatwia dodanie buforowania i testów w przyszłości.
Często zadawane pytania
DataSource — klasa pracująca z jednym źródłem (API, BD, pamięć podręczna). Repository — klasa zarządzająca wieloma DataSource i udostępniająca jednolity interfejs. Repository decyduje, z którego DataSource pobrać dane, i koordynuje buforowanie. DataSource nie wie o istnieniu innych źródeł, Repository nie zna szczegółów implementacji każdego źródła.
Tak, Repository jest przydatne w SwiftUI do oddzielenia danych od View. ViewModel subskrybuje Publisher z Repository, a Repository zarządza buforowaniem i synchronizacją. W prostych aplikacjach można używać URLSession bezpośrednio w ViewModel, ale dla testowalności i skalowalności Repository jest preferowane. Apple nie narzuca tego wzorca, ale jest on zgodny z SwiftData i Network.framework.
DataSource są zastępowane obiektami mock przez Dependency Injection. Test tworzy mock RemoteDataSource (zwracający predefiniowany JSON) i mock LocalDataSource (sprawdzający, czy dane zostały zapisane). Repository jest testowane izolowanie: sprawdzana jest strategia buforowania, obsługa błędów i prawidłowa kolejność wywołań. Do testów integracyjnych używa się TestDispatcher (Kotlin) lub MainActor.run (Swift).
Można, ale nie jest zalecane. Bez protokołu nie można podmienić implementacji w testach ani w podglądzie. W Kotlin interfejs Repository umożliwia podmianę implementacji przez DI (Dagger, Hilt, Koin). W Swift protokół Repository jest obowiązkowy do testowania kodu async-await i Combine. Wyjątkiem są proste projekty z jednym źródłem danych, gdzie Repository nie zawiera logiki buforowania.
Offline-first — strategia, w której aplikacja działa bez internetu, korzystając z danych lokalnych. Repository odgrywa kluczową rolę: najpierw zwraca dane z lokalnego DataSource, a następnie synchronizuje się z serwerem w tle. Użytkownik widzi dane natychmiast, a Repository aktualizuje je po pobraniu z sieci. Room z Flow zapewnia reaktywną aktualizację UI przy zmianie danych w lokalnej bazie.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również