DEV Community

Cover image for Flutter Repository Pattern Explained (Stop Accessing APIs Directly)
Shashi Kant
Shashi Kant

Posted on

Flutter Repository Pattern Explained (Stop Accessing APIs Directly)

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');
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Your app depends on abstraction, not implementation.


πŸ“¦ Step 1: Define Repository Contract (Domain Layer)

abstract class UserRepository {
  Future<User> getUser(int id);
}
Enter fullscreen mode Exit fullscreen mode

βœ” 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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'],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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()));
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ BLoC has no idea where data comes from.


🎯 Why This Matters (Real Benefits)

βœ… 1. Swap API β†’ Local DB easily

You can replace:

UserRemoteDataSource
Enter fullscreen mode Exit fullscreen mode

with:

UserLocalDataSource
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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)