The Adapter Pattern is one of the foundational patterns introduced in the legendary “Gang of Four” (GoF) book — Design Patterns: Elements of Reusable Object-Oriented Software

It’s a timeless design approach that allows software components with incompatible interfaces to work together seamlessly.

Whether you’re coming from iOS, Android, backend, or frontend, understanding design patterns like Adapter is a transferrable skill that transcends languages and frameworks.

Good architecture thinking stays the same — only the syntax changes.

As someone who has worked extensively on iOS projects, I found that this pattern, which is deeply ingrained in UIKit and Swift architectures (often seen as Delegation), translates naturally into Flutter.

This article demonstrates exactly that — how a GoF pattern used for decades can elegantly solve a common Flutter problem.


🧠 What Is the Adapter Pattern?

At its core, the Adapter pattern acts as a bridge between two classes that otherwise can’t communicate directly because their interfaces don’t match.

Think of it as a translator between two systems:

one class expects a certain set of methods, and another provides functionality that needs to be adapted to match that expectation.

In essence:

  • It decouples components that shouldn’t depend on each other directly.
  • It promotes flexibility and testability.
  • It lets you reuse existing classes without modifying them.

🧩 The Problem: Communicating Between ViewModels in Flutter

Imagine a Flutter app that displays a feed of wallpapers. (well, currently I have been developing this app for fun, called Sketra, soon will be available on both App Store and Play Store)

Each wallpaper can be marked as a favorite. When you tap on a wallpaper, you open a detail page where you can favorite or unfavorite it.

Here’s the challenge:

  • The FeedDetailPage changes the favorite state of a wallpaper.
  • But when returning to the FeedPage, the list doesn’t automatically reflect that change.

Naively, we might:

  • Use a global provider or shared state, or
  • Re-fetch the entire feed after returning.

Those solutions work, but they’re not clean — they add unnecessary complexity and coupling.

Instead, we’ll use the Adapter Pattern to allow the FeedDetailViewModel to communicate with the FeedViewModel directly, without them knowing about each other.


⚙️ The Adapter in Action

Here’s what the communication flow looks like:

FeedDetailViewModel → FeedDetailViewModelDelegate (abstract)
                                       ↓
                           FeedPageToggleAdapter (adapter)
                                       ↓
                                FeedViewModel

Let’s break it down.


1️⃣ Define the Delegate Interface

We start by defining an abstract contract for communication.

abstract class FeedDetailViewModelDelegate {
  void didToggleFavorite(WallpaperEntity wallpaper);
}

This interface defines what can happen — but not how.

It allows the detail screen to notify a delegate that something has changed, without knowing who the delegate is.


2️⃣ The Detail ViewModel

The FeedDetailViewModel performs the logic for toggling favorites and calls its delegate when done.

class FeedDetailViewModel extends ChangeNotifier {
  ...
  final FeedDetailViewModelDelegate _delegate;

  FeedDetailViewModel(
    ...,
    this._delegate,
  );

  WallpaperEntity? wallpaper;

  Future<void> toggleFavorite() async {
    if (wallpaper == null) return;

    final isFavorite = await _checkIsFavoriteWallpaperUseCase.execute(wallpaper!);

    if (isFavorite) {
      await _unfavoriteWallpaperUseCase.execute(wallpaper!);
    } else {
      await _favoriteWallpaperUseCase.execute(wallpaper!);
    }

    notifyListeners();

    // Notify the delegate (adapter bridge)
    _delegate.didToggleFavorite(wallpaper!);
  }
}

The delegate is injected from the outside, so this ViewModel never needs to know what’s on the other side.


3️⃣ The Adapter Implementation

This adapter conforms to the FeedDetailViewModelDelegate interface and connects back to the FeedViewModel.

final class FeedPageToggleAdapter implements FeedDetailViewModelDelegate {
  final FeedViewModel _feedViewModel;

  FeedPageToggleAdapter(this._feedViewModel);

  @override
  void didToggleFavorite(WallpaperEntity wallpaper) {
    _feedViewModel.toggleFavorite(wallpaper);
  }
}

This is the “translator” — the bridge between the detail logic and the feed logic.


4️⃣ The Feed ViewModel

The FeedViewModel maintains a list of favorite IDs and exposes a simple toggleFavorite method.

class FeedViewModel extends ChangeNotifier {
  final Set<String> _favoriteIds = {};

  bool isFavorite(WallpaperEntity wallpaper) =>
      _favoriteIds.contains(wallpaper.id);

  void toggleFavorite(WallpaperEntity wallpaper) {
    if (isFavorite(wallpaper)) {
      _favoriteIds.remove(wallpaper.id);
    } else {
      _favoriteIds.add(wallpaper.id);
    }
    notifyListeners();
  }
}

It stays completely unaware of the detail logic — maintaining perfect separation of concerns.


5️⃣ Wiring It All Together

When we navigate to the detail page, we inject the adapter that connects both ViewModels.

Future<dynamic> _showFeedDetailPage(
  BuildContext context,
  FeedViewModel feedViewModel,
  WallpaperEntity wallpaper,
) {
  return Navigator.push(
    context,
    MaterialPageRoute(
      builder: (_) {
        final favoriteStore = context.read<HiveFavoriteWallpaperStore>();
        final wallpaperService = context.read<JsonWallpaperService>();

        final viewModel = FeedDetailViewModel(
          wallpaper.id,
          wallpaperService,
          DownloadWallpaperService(),
          DefaultCheckIsFavoriteWallpaperUseCase(favoriteStore),
          DefaultFavoriteWallpaperUseCase(favoriteStore),
          DefaultUnfavoriteWallpaperUseCase(favoriteStore),
          FeedPageToggleAdapter(feedViewModel), // 👈 adapter injected
        );

        return FeedDetailPage(viewModel: viewModel);
      },
    ),
  );
}

Now, when the user toggles a favorite inside the detail screen:

  • The FeedDetailViewModel calls _delegate.didToggleFavorite(wallpaper).
  • The FeedPageToggleAdapter forwards it to the FeedViewModel.
  • The feed updates immediately — with no global state and no messy context lookups.

🧭 Why This Matters

Decoupled design — ViewModels don’t know about each other.

Reusability — You can reuse the detail logic in other screens easily.

Testability — Mock the delegate in unit tests to verify communication.

Cross-platform skill — Understanding Adapter pattern makes you a better engineer, not just a better Flutter dev.

Scalability — This pattern scales beautifully when your app grows.


🧩 In Summary

The Adapter Pattern isn’t just an academic GoF concept — it’s a practical tool for real-world Flutter projects.

It helps you maintain clean boundaries between components while enabling them to cooperate effectively.

Even if your background is in iOS, Android, or backend systems, these architectural ideas are universal and transferrable.

This small example proves how solid object-oriented design remains timeless — whether in Swift, Kotlin, or Dart.

References :

Leave a comment