VOOZH about

URL: https://dev.to/skntin/flutter-repository-pattern-explained-stop-accessing-apis-directly-49ah

⇱ Flutter Repository Pattern Explained (Stop Accessing APIs Directly) - DEV Community


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 πŸš€