Flutter Repository Pattern Explained (Stop Accessing APIs Directly)

Published: (March 31, 2026 at 10:45 AM EDT)
3 min read
Source: Dev.to

Source: Dev.to

Cover image for 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');

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:

UserRemoteDataSource

with a local one:

UserLocalDataSource

No 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

0 views
Back to Blog

Related posts

Read more »

Api Structure with Http

Dealing with Asynchronously Note that the HTTP APIs use Dart Futures in the return values. We recommend using the API calls with the async/await syntax. 1. Cre...