Repositories
Repositories are a fundamental part of app architecture, acting as an intermediary between the data layer and the rest of the application. In Flutter, repositories streamline how data is fetched, stored, and modified.They provide a single access point for data, whether it comes from remote APIs or local databases, ensuring that business logic is consistently applied across your app.
This article will explain the role of repositories in Flutter and provide examples for each key responsibility.Repositories follow the principle of “separation of concerns.” This principle means that different parts of your app have distinct responsibilities, such as presenting data (UI), holding data (repositories), and interacting with external services or databases (data sources).
The repository sits at the heart of this system, ensuring that the UI is decoupled from the data storage and retrieval logic. It provides a well-structured interface to fetch and modify data without worrying about the underlying data sources.
Exposing Data to the Rest of the App
One of the core responsibilities of a repository is to expose data to other parts of the app. Repositories abstract away the data-fetching logic, ensuring that the rest of the app doesn’t need to worry about where or how the data is being retrieved.Whether the data comes from an API, a local database, or even hardcoded values, the repository provides a single access point for retrieving and returning this data to the UI.By centralizing access to data, repositories simplify the process for developers. They don’t need to know or care about whether the data comes from a network call or is cached locally—the repository handles these decisions internally.
This provides a clean and uniform API for the rest of the app to interact with.
class ProductRepository {
final ProductRemoteDataSource remoteDataSource;
final ProductLocalDataSource localDataSource;
ProductRepository(this.remoteDataSource, this.localDataSource);
Future<Product> getProductById(int id) async {
try {
// Try fetching product from remote
final product = await remoteDataSource.fetchProductById(id);
return product;
} catch (error) {
// Fallback to local storage if remote fails
return localDataSource.getProductById(id);
}
}
}
Resolving Conflicts Between Multiple Data Sources
When an application works with multiple data sources—such as remote APIs and local databases—repositories are responsible for managing conflicts that can arise between these sources.For example, the repository may need to decide whether to prioritize data from the network or rely on locally cached data when there’s no internet connection.
The repository also handles syncing changes between local and remote sources, ensuring that the app always has the most up-to-date information.This conflict resolution is crucial because it ensures data consistency and avoids situations where the app might show outdated or incomplete information.
The repository manages which data source takes precedence and under what circumstances, keeping the logic centralized and reusable throughout the app.
class ProductRepository {
final ProductRemoteDataSource remoteDataSource;
final ProductLocalDataSource localDataSource;
ProductRepository(this.remoteDataSource, this.localDataSource);
Stream<Product> getProductById(int id) async* {
// First yield the product from the local data source (cached data)
final localProduct = localDataSource.getProductById(id);
if (localProduct != null) {
yield localProduct;
}
// Then try to fetch the product from the remote data source
if (isConnectingToInternet()) {
final remoteProduct = await remoteDataSource.fetchProductById(id);
yield remoteProduct;
// Update local cache with the new remote product
await localDataSource.saveProduct(remoteProduct);
} else {
print("Failed to fetch remote product, using cached data");
}
}
}
Centralizing Changes Across Data Sources
Repositories also ensure that any changes made to data are consistently applied across all data sources.For example, if a user updates a product, the repository ensures that this change is reflected in both the local database and the remote server.
This centralization is key to maintaining data consistency within the app.Without a repository, it would be easy to overlook one data source when making updates, which could lead to inconsistencies in the app.
Repositories handle the synchronization between data sources, so the developer only needs to update the data in one place (the repository), and the change will propagate to all sources.
class ProductRepository {
final ProductRemoteDataSource remoteDataSource;
final ProductLocalDataSource localDataSource;
ProductRepository(this.remoteDataSource, this.localDataSource);
Future<void> updateProduct(Product product) async {
// Update local database
await localDataSource.updateProduct(product);
// Sync the updated product to the remote server
try {
await remoteDataSource.updateProduct(product);
} catch (error) {
print("Failed to sync with remote");
}
}
}
Containing Business Logics
Repositories don’t just handle data storage and retrieval—they also encapsulate the business logic that governs how data should be used.Business logic refers to the rules and processes that determine how data is created, read, updated, or deleted (CRUD operations).
For example, you might need to validate product prices, check inventory before completing an order, or filter data based on user preferences.
Keeping business logic inside repositories keeps your codebase organized and allows you to separate concerns between the UI and the data layer.When business logic is centralized within the repository, it becomes easier to maintain and test.
You can focus on the UI layer for rendering and user interaction, while the repository takes care of all the rules and constraints related to data handling.
class ProductRepository {
final ProductRemoteDataSource remoteDataSource;
final ProductLocalDataSource localDataSource;
ProductRepository(this.remoteDataSource, this.localDataSource);
Future<List<Product>> getDiscountedProducts() async {
// Fetch all products from the remote data source
final products = await remoteDataSource.fetchAllProducts();
// Apply business logic: only return products with a discount
return products.where((product) => product.price < 50.0).toList();
}
}