Repository Pattern — un pattern che aggiunge un livello di astrazione tra la logica di business e le fonti di dati. Invece di chiamare direttamente API, database o cache, il Repository fornisce un'interfaccia unificata per ottenere e memorizzare i dati. Questo semplifica i test e il passaggio tra le fonti. Leggi di più nella documentazione Android Data Layer.
Punti chiave
Repository Pattern è un pattern strutturale che isola la logica di business dall'accesso diretto alle fonti di dati. Invece che una Activity, UIViewController o ViewModel chiami direttamente Retrofit, URLSession, Room o CoreData, comunicano con il Repository. Il Repository decide da dove ottenere i dati — dalla rete, dal database o dalla cache — e restituisce il risultato in un formato unificato. Questo implementa il principio di responsabilità singola — la UI non sa come o da dove sono stati ottenuti i dati.
I componenti del Repository includono un'interfaccia (protocollo), un'implementazione e uno o più DataSources. Un DataSource è una classe che lavora con una singola fonte: RemoteDataSource chiama l'API tramite un client HTTP, LocalDataSource legge e scrive nel database. Il Repository riceve i DataSources tramite il costruttore (Dependency Injection) e decide quale fonte utilizzare. Ad esempio, quando richiede una lista di utenti, il Repository controlla prima la cache, poi il database, poi la rete.
Vantaggi del Repository Pattern: l'isolamento dei cambiamenti delle fonti di dati (cambiamenti API, migrazioni DB) non influisce sul livello UI; test unitari tramite sostituzione del Repository o DataSource; caching trasparente per la UI; passaggio tra modalità online e offline senza modificare la logica dello schermo. La community Android raccomanda Repository come livello obbligatorio in Clean Architecture.
Implementazione iOS del Repository è costruita sui protocolli Swift. Il protocollo Repository dichiara metodi per ottenere e memorizzare i dati. L'implementazione reale viene iniettata tramite l'inizializzatore — questo consente di sostituire l'implementazione nei test e nelle anteprime SwiftUI. Anche i DataSources sono dichiarati come protocolli: Protocol RemoteDataSource, Protocol LocalDataSource. La ViewModel o l'Interactor non conosce un'implementazione specifica — solo il protocollo 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 in iOS per Repository è tipicamente configurata tramite una factory o un contenitore DI (Swinject, Factory). Nei test, il protocollo UserRepository viene sostituito con un'implementazione mock che restituisce dati predefiniti. Async-await rende il codice sincrono e leggibile senza closures e delegate. Per la reattività con Combine, i metodi del Repository restituiscono AnyPublisher invece di async throws.
Implementazione Android del Repository utilizza ampiamente Kotlin Coroutines e Flow per operazioni asincrone. Google raccomanda Repository nella guida ufficiale all'architettura Android (Android Architecture Components). Il Repository accetta RemoteDataSource (Retrofit) e LocalDataSource (Room) tramite il costruttore, e la ViewModel si sottoscrive a un Flow dal Repository. Il Repository gestisce la strategia dei dati: cache-first, network-first, o sempre rete con scrittura in cache.
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))
}
}
}
Wrapper Result nell'esempio sopra è standard per Android: una classe sealed Result informa la ViewModel sullo stato di caricamento (Loading, Success, Error). La ViewModel si sottoscrive tramite collect e aggiorna StateFlow o LiveData. Repository con Flow notifica automaticamente la UI dei cambiamenti nel database — questa è una differenza chiave rispetto alle richieste singole dove la UI non viene a conoscenza dei cambiamenti senza aggiornamento manuale.
DataSource — classi responsabili di lavorare con una specifica fonte di dati. RemoteDataSource utilizza un client HTTP (URLSession, Retrofit, Ktor) per ottenere dati dall'API. LocalDataSource lavora con l'archiviazione locale (CoreData, Realm, Room, UserDefaults, DataStore). Ogni DataSource ha una responsabilità ristretta: RemoteDataSource conosce solo il formato della richiesta API, LocalDataSource — lo schema del database. Il Repository li combina, implementando una strategia di caching.
| DataSource | Piattaforma iOS | Piattaforma Android | Fonte |
|---|---|---|---|
| Remote | URLSession + Codable | Retrofit + Moshi/Gson | API REST / GraphQL |
| Local (DB) | CoreData, SwiftData | Room, SQLDelight | SQLite sul dispositivo |
| Local (cache) | NSCache, UserDefaults | DataStore, EncryptedSP | In memoria / disco |
| Preferenze | UserDefaults, Keychain | SharedPreferences, EncryptedSP | Impostazioni, token |
Strategie di caching in Repository: Cache-First (cache prima, poi caricamento in background), Network-Only (solo rete, per schermate di pagamento), Network-First-With-Cache-Backup (rete prima, fallback alla cache in caso di errore). La scelta della strategia dipende dallo scenario: una lista di paesi può essere memorizzata nella cache a lungo, i tassi di cambio — per 15 minuti, il saldo del portafoglio — solo dalla rete. Il Repository implementa la strategia e la modifica senza alterare ViewModel o UI.
Repository e Service sono pattern diversi con funzioni sovrapposte. Repository è responsabile dell'accesso ai dati e del caching, restituendo modelli di dati. Service (o Interactor, Use Case) contiene logica di business: validazione, trasformazione dei dati, orchestrazione di chiamate a più Repository. Service può combinare UserRepository, OrderRepository e NotificationRepository per elaborare un ordine. Repository non contiene logica di business — solo CRUD e caching.
Quando scegliere Repository — navigazione dei dati con più fonti (API + DB + cache), architettura offline-first, necessità di caching e cambio trasparente delle fonti. Repository è obbligatorio in Clean Architecture e raccomandato da Google per le applicazioni Android. Nell'architettura VIPER su iOS, il ruolo di Repository è svolto dal livello Interactor, che interagisce con Manager o Service per l'accesso ai dati.
Quando Service è sufficiente — applicazioni semplici con una singola fonte di dati, schermate di sola lettura senza scrittura, progetti senza modalità offline. In tali casi, DataSource viene utilizzato direttamente da ViewModel o Presenter e Repository diventa un livello superfluo. Tuttavia, aggiungere Repository in una fase iniziale non richiede molto sforzo e semplifica l'aggiunta futura di caching e test.
Domande frequenti
DataSource è una classe che lavora con una singola fonte (API, DB, cache). Repository è una classe che gestisce più DataSources e fornisce un'interfaccia unificata. Il Repository decide quale DataSource utilizzare e coordina il caching. DataSource non conosce l'esistenza di altre fonti; Repository non conosce i dettagli implementativi di ogni fonte.
Sì, Repository è utile in SwiftUI per separare i dati dalla View. La ViewModel si sottoscrive a un Publisher dal Repository, e il Repository gestisce il caching e la sincronizzazione. Nelle applicazioni semplici, è possibile utilizzare URLSession direttamente nella ViewModel, ma per testabilità e scalabilità, Repository è preferibile. Apple non impone il pattern, ma è compatibile con SwiftData e Network.framework.
I DataSources vengono sostituiti con oggetti mock tramite Dependency Injection. Il test crea un RemoteDataSource mock (restituisce JSON predefinito) e un LocalDataSource mock (verifica che i dati siano salvati). Il Repository viene testato isolatamente: la strategia di caching, la gestione degli errori e l'ordine corretto delle chiamate vengono verificati. Per i test di integrazione si utilizzano TestDispatcher (Kotlin) o MainActor.run (Swift).
È possibile ma non raccomandato. Senza un protocollo, è impossibile sostituire l'implementazione nei test e nelle anteprime. In Kotlin, l'interfaccia Repository consente di sostituire l'implementazione tramite DI (Dagger, Hilt, Koin). In Swift, il protocollo Repository è obbligatorio per testare codice async-await e Combine. L'eccezione sono i progetti semplici con un'unica fonte di dati dove Repository non ha logica di caching.
Offline-first è una strategia in cui l'applicazione funziona senza Internet utilizzando dati locali. Repository gioca un ruolo chiave: prima restituisce i dati dal DataSource locale, poi si sincronizza con il server in background. L'utente vede i dati istantaneamente e Repository li aggiorna dopo il caricamento dalla rete. Room con Flow fornisce aggiornamenti reattivi della UI quando i dati cambiano nel database locale.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche