Understanding the Repository Pattern in Flutter
The Repository pattern in Flutter abstracts data access behind a type‑safe interface, isolating domain models from API, database, or device details, enabling easy swapping of implementations, simplifying testing with mocks, and promoting clean architecture by keeping UI code separate from business and networking logic.
Design patterns are useful templates that help solve common software design problems. In the context of application architecture, structural design patterns guide how to organize the different parts of an app.
The Repository pattern abstracts data access (e.g., from a backend API) and provides type‑safe entities to the domain layer, keeping the rest of the application independent of data‑source details.
What is the Repository Pattern?
A repository sits in the data layer. Its responsibilities are:
Isolate domain models (or entities) from the implementation details of data sources.
Convert data‑transfer objects into domain‑layer entities.
(Optional) Perform caching or other data‑layer operations.
The diagram shown is just one possible architecture. If you use MVC, MVVM, or Clean Architecture the placement may differ, but the concept remains the same.
Widgets belong to the presentation layer and should not contain business logic or network code.
Do not mix UI code with business logic; keep them separate for easier testing and maintenance.
When to Use the Repository Pattern?
It is especially handy when an app has a complex data layer with many endpoints returning unstructured data (e.g., JSON) that you want to isolate from the rest of the app.
Typical use cases include:
Communicating with a REST API.
Communicating with a local or remote database (e.g., Sembast, Hive, Firestore).
Communicating with device‑specific APIs (permissions, camera, location, etc.).
If a third‑party API changes, you only need to update the repository implementation.
Practical Repository Pattern in a Flutter Weather App
The following example shows a simple Flutter app that fetches weather data from the OpenWeatherMap API.
First, define an abstract repository interface:
abstract class WeatherRepository {
Future
getWeather({required String city});
}Then implement it with a concrete class that uses an HTTP client:
import 'package:http/http.dart' as http;
class HttpWeatherRepository implements WeatherRepository {
HttpWeatherRepository({required this.api, required this.client});
// custom class defining all the API details
final OpenWeatherMapAPI api;
// client for making calls to the API
final http.Client client;
// implements the method in the abstract class
Future
getWeather({required String city}) {
// TODO: send request, parse response, return Weather object or throw error
}
}Define a model class for the JSON response:
class Weather {
// TODO: declare all the properties we need
factory Weather.fromJson(Map
json) {
// TODO: parse JSON and return validated Weather object
}
}Initialize the repository using a dependency‑injection solution. Example with get_it :
import 'package:get_it/get_it.dart';
GetIt.instance.registerLazySingleton
(
() => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client()),
);Or with Riverpod:
import 'package:flutter_riverpod/flutter_riverpod.dart';
final weatherRepositoryProvider = Provider
((ref) {
return HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client());
});Or with flutter_bloc:
import 'package:flutter_bloc/flutter_bloc.dart';
RepositoryProvider
(
create: (_) => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client()),
child: MyApp(),
);Abstract Class vs. Concrete Class
When creating a repository, you may wonder whether an abstract class is necessary.
Using an abstract class gives you a single place to view the repository contract and makes swapping implementations (e.g., DioWeatherRepository instead of HttpWeatherRepository ) trivial. The downside is a bit more boilerplate and potential IDE navigation confusion.
Using only a concrete class reduces boilerplate and makes “go to definition” point directly to the implementation, but changing the implementation later requires more code changes.
Testing the Repository
In tests you often replace network code with a fake or mock. Because Dart classes already have an implicit interface, you can mock the concrete class directly.
class FakeWeatherRepository implements HttpWeatherRepository {
Future
getWeather({required String city}) {
return Future.value(Weather(...));
}
}Using mocktail :
import 'package:mocktail/mocktail.dart';
class MockWeatherRepository extends Mock implements HttpWeatherRepository {}
final mockWeatherRepository = MockWeatherRepository();
when(() => mockWeatherRepository.getWeather('London'))
.thenAnswer((_) => Future.value(Weather(...)));Or mock the underlying http.Client :
class MockHttpClient extends Mock implements http.Client {}
void main() {
test('repository with mocked http client', () async {
final mockHttpClient = MockHttpClient();
final api = OpenWeatherMapAPI();
final weatherRepository = HttpWeatherRepository(api: api, client: mockHttpClient);
when(() => mockHttpClient.get(api.weather('London')))
.thenAnswer((_) => Future.value(/* some valid http.Response */));
final weather = await weatherRepository.getWeather(city: 'London');
expect(weather, Weather(...));
});
}Choose whether to mock the repository itself or the lower‑level data source based on what you want to verify.
Minimal Interface and Horizontal Scaling
If you force every data source behind a single interface, you may end up with a “least common denominator” that limits functionality (e.g., streaming vs. one‑shot requests). Consider creating multiple repositories for distinct domains (e.g., product, cart, orders) as the app grows.
Keep It Simple
Don’t over‑engineer the API. Define the repository interface that matches the APIs you need, and refactor later if requirements change.
Conclusion
Use the Repository pattern to hide data‑layer implementation details (such as JSON serialization) from the rest of the app. This yields a more resilient codebase that can withstand breaking changes in third‑party packages.
The article was translated from https://codewithandrea.com/articles/flutter-repository-pattern/.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.