Unlocking Clean Code: The Power of the Repository Pattern in Flutter
What is the Repository Pattern?
The repository pattern is a software design pattern that decouples data access logic from business logic by introducing a centralized component called a repository. This pattern consists of three interconnected components: the client, repository, and data source.
The Benefits of the Repository Pattern
The repository pattern offers several key benefits, including:
- Centralized data access
- Decoupling of data layer from business logic
- Improved unit-testability
- Easy switching of data sources
Implementing the Repository Pattern in Flutter
To implement the repository pattern in Flutter, we’ll create a simple bookstore application with a CRUD-based repository pattern. We’ll use a virtual data layer (a mock database provider) as our data provider infrastructure.
Defining Models and Setting Up the Repository
We’ll start by defining a model for the Book business entity with several properties and methods.
class Book {
final int id;
final String title;
final String author;
Book({required this.id, required this.title, required this.author});
}
Then, we’ll create a virtual data access layer using Dart Maps, and implement several functions to add, edit, remove, and retrieve key-value-based data records.
class VirtualDataLayer {
final Map<int, Book> _books = {};
Future<void> addBook(Book book) async {
_books[book.id] = book;
}
Future<Book?> getBook(int id) async {
return _books[id];
}
//...
}
Creating the Book Repository
Next, we’ll create a repository class for the Book business entity. We’ll define a base repository interface and then implement a concrete implementation for the book repository using the interface definition.
abstract class BaseRepository<T> {
Future<List<T>> getAll();
Future<T?> get(int id);
Future<void> add(T item);
Future<void> update(T item);
Future<void> delete(int id);
}
class BookRepository implements BaseRepository<Book> {
final VirtualDataLayer _dataLayer;
BookRepository(this._dataLayer);
@override
Future<List<Book>> getAll() async {
//...
}
@override
Future<Book?> get(int id) async {
//...
}
@override
Future<void> add(Book book) async {
await _dataLayer.addBook(book);
}
//...
}
Using the Repository from the Flutter Application Frontend
Now that our book repository is ready, we’ll create a frontend for our bookstore app. We’ll create a controller/service to manipulate data from the widget level, and then build the UI for the bookstore app using the controller instance.
class BookController {
final BookRepository _repository;
BookController(this._repository);
Future<void> loadBooks() async {
final books = await _repository.getAll();
//...
}
Future<void> addBook(Book book) async {
await _repository.add(book);
//...
}
//...
}
Writing Unit Tests for the Repository Pattern
To write unit tests for the repository pattern, we can implement a mock repository class and test the controller/service logic, or implement a mock database class and test the repository logic.
class MockBookRepository extends Mock implements BookRepository {
//...
}
void main() {
group('BookController', () {
test('loads books', () async {
final repository = MockBookRepository();
final controller = BookController(repository);
await controller.loadBooks();
//...
});
test('adds book', () async {
final repository = MockBookRepository();
final controller = BookController(repository);
final book = Book(id: 1, title: 'Test Book', author: 'John Doe');
await controller.addBook(book);
//...
});
});
}
Creating Multiple Repositories
When working with large-scale Flutter apps, we may need to manage multiple business entities. To create multiple repositories, we can define our models, create a generic interface for the base repository definition, and then write multiple concrete repositories or one generic concrete repository.
DAO vs. Repository vs. Service Patterns
The repository pattern is often confused with DAO and service patterns, but there are several noticeable differences between them. The repository pattern provides a way to organize code by creating another abstraction layer, whereas DAO and service patterns serve different purposes.