Flutter Repository Pattern Explained (Stop Accessing APIs Directly)
Source: Dev.to

If your BLoC is calling APIs directly…
👉 your architecture is already broken.
It might work today — but as your app grows, it turns into a nightmare:
- Hard to test ❌
- Hard to scale ❌
- Impossible to swap data sources ❌
Let’s fix that properly.
🧠 The Real Problem
Most Flutter apps look like this:
final response = await dio.get('/users');The call often lives inside:
- BLoC ❌
- UI ❌
- Even widgets ❌
👉 This creates tight coupling between your app and your API.
🏗️ The Solution: Repository Pattern
The repository acts as a bridge between:
- Data sources (API, local DB)
- Domain layer (business logic, BLoC)
UI → Bloc → UseCase → Repository → DataSource👉 Your app depends on abstraction, not implementation.
📦 Step 1: Define Repository Contract (Domain Layer)
abstract class UserRepository {
Future getUser(int id);
}- No API code
- No JSON handling
- Pure business‑logic contract
🔌 Step 2: Create Data Source (Data Layer)
class UserRemoteDataSource {
final Dio dio;
UserRemoteDataSource(this.dio);
Future> fetchUser(int id) async {
final response = await dio.get('/users/$id');
return response.data;
}
}👉 This is the only place that talks to your API.
🔄 Step 3: Implement Repository
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
UserRepositoryImpl(this.remoteDataSource);
@override
Future getUser(int id) async {
final data = await remoteDataSource.fetchUser(id);
return User(
id: data['id'],
name: data['name'],
email: data['email'],
);
}
}👉 Converts raw data → Entity.
🧩 Step 4: Use It in BLoC
class UserBloc extends Bloc {
final UserRepository repository;
UserBloc(this.repository) : super(UserInitial()) {
on((event, emit) async {
emit(UserLoading());
try {
final user = await repository.getUser(event.id);
emit(UserLoaded(user));
} catch (e) {
emit(UserError(e.toString()));
}
});
}
}👉 BLoC has no idea where the data comes from.
🎯 Why This Matters (Real Benefits)
✅ 1. Swap API → Local DB easily
Replace the remote data source:
UserRemoteDataSourcewith a local one:
UserLocalDataSourceNo changes required in the BLoC.
✅ 2. Testing becomes EASY
class MockUserRepository implements UserRepository {
@override
Future getUser(int id) async {
return User(id: 1, name: 'Test', email: 'test@mail.com');
}
}👉 No API calls in tests. Ever.
✅ 3. Scales like a real production app
You can add:
- Caching
- Multiple APIs
- Offline mode
without breaking your app.
🚨 Common Mistakes
- Returning JSON from the repository
- Calling Dio inside BLoC
- Mixing model & entity
- Skipping abstraction “to save time”
👉 These kill scalability.
💡 Pro Tip (Most Important)
Repository should return Entities, not Models.
- Model → Data layer
- Entity → Domain layer
🔚 Final Thoughts
The repository pattern is not “extra code”.
👉 It’s what separates:
- Small apps
- from
- Production systems
Next article: Stop Throwing Exceptions ❌ Proper Error Handling in Flutter Clean Architecture