Flutter Repository Pattern Explained (Stop Accessing APIs Directly)
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');
Inside:
- BLoC β
- UI β
- Even widgets sometimes β
π 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<User> getUser(int id);
}
β No API
β No JSON
β Pure business logic contract
π Step 2: Create Data Source (Data Layer)
class UserRemoteDataSource {
final Dio dio;
UserRemoteDataSource(this.dio);
Future<Map<String, dynamic>> 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<User> 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<UserEvent, UserState> {
final UserRepository repository;
UserBloc(this.repository) : super(UserInitial()) {
on<FetchUser>((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 data comes from.
π― Why This Matters (Real Benefits)
β 1. Swap API β Local DB easily
You can replace:
UserRemoteDataSource
with:
UserLocalDataSource
No changes in BLoC.
β 2. Testing becomes EASY
class MockUserRepository implements UserRepository {
@override
Future<User> 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 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
Follow me to continue the series π
Top comments (0)