Structural Design Patterns for Dart and Flutter: Adapter

With the Adapter pattern, you create an intermediary between two incompatible class interfaces, allowing one to work with the other. This works very much like physical adapters in the real world, such as those that attach to a power cord to enable a device to draw power from a foreign power outlet, or those that enable you to read data from one type of memory card in a drive made for another type or size. The intermediary class acts as a bridge between your client code and another class. You could also think of the adapter class as a wrapper around an object of another class.

About structural design patterns: Structural patterns help us shape the relationships between the objects and classes we create. These patterns are focused on how classes inherit from each other, how objects can be composed of other objects, and how objects and classes interrelate. In this series of articles, you'll learn how to build large, comprehensive systems from simpler, individual modules and components. The patterns assist us in creating flexible, loosely coupled, interconnecting code modules to complete complex tasks in a manageable way.

There are two different Adapter patterns. The first is called the Class Adapter pattern, but it relies on multiple inheritance, a feature Dart does not support. Class Adapter uses inheritance to adapt one interface to another, a technique that's falling by the wayside in favor of composition. For these reasons, we'll focus on the Object Adapter pattern, which is fully supported in Dart, and it's the most powerful and flexible of the two approaches.

The code for this article was tested with Dart 2.8.4 and Flutter 1.17.5.

Let's look at an example of the Adapter pattern in action.

A simple Adapter example

Imagine you're building an app that needs to make use of an older shape library for rendering shapes on the screen. In this library, rectangles are defined by specifying their 2D position, along with a width and height. In your app, it would be more convenient to be able to create rectangles from four coordinates instead. You can use the Adapter pattern to create a wrapper around the old library's rectangle class. The adapter will take your app's preferred arguments and automatically adapt them for use with the old rectangle class. Let's see how it's done:

class OldRectangle {
  final int x;
  final int y;
  final int width;
  final int height;

  const OldRectangle(this.x, this.y, this.width, this.height);

  void render() {
    print("Rendering OldRectangle...");
  }
}

class Rect {
  OldRectangle _oldRect;

  Rect(int left, int top, int right, int bottom) {
    _oldRect = OldRectangle(left, top, right - left, bottom - top);
  }

  void render() => _oldRect.render();
}

In this example, the Rect class is the adapter, wrapping a private instance of OldRectangle. A client can use Rect, passing the types of arguments it finds convenient, and Rect will adapt the input to the needs of OldRectangle. When the client calls the Rect class's render() method, the call is invisibly redirected to the one in OldRectangle. If these were real graphics classes, rendering either version would produce an identical rectangle on the screen, but through different interfaces.

It should be noted that the Adapter pattern is often implemented with an explicit interface for the wrapper, but I've left that out of this example. Dart doesn't support explicit interfaces, since every class implicitly exports an interface. You can create an abstract class to do a similar job, but there is little to be gained from doing so in this simple example.

Next up, we'll look at a more advanced example that uses an adapter interface.

An advanced Adapter example

In this example, we'll pretend to be making an app that aggregates social media posts from multiple sites. These posts will come from several disparate sources, each with its own API and data format. We can use the Adapter pattern to keep things organized and conceal the complexity inherent in supporting multiple protocols.

First, let's look at our Post model:

class Post {
  final String title;
  final String content;

  const Post(this.title, this.content);
}

This simple model defines properties to hold a title and some kind of text content. The constructor simply sets those properties from passed arguments. No matter where posts come from, the app needs them to end up in this format.

Next, here are two mock APIs for retrieving posts:

class SiteApi1 {
  String getSite1Posts() {
    return '[{"headline": "Title1", "text": "Sample text..."}]';
  }
}

class SiteApi2 {
  String getSite2Posts() {
    return '[{"header": "Title1", "body": "Sample text..."}]';
  }
}

Note that each API has a different method for acquiring posts, and though they both return content in JSON format, the property names differ. In a real app, the methods may take different arguments to perform their functions, and obviously they would usually return much more content. What our app needs is a consistent way to get posts, and those posts should always be delivered encased in the Post model.

To achieve this, we start by creating an interface:

abstract class IPostsAPI {
  List<Post> getPosts();
}

As previously mentioned, Dart has no support for explicit interfaces, but since all classes export their interfaces, we can use an abstract class to achieve a similar result. An abstract class cannot be instantiated, and it cannot include implementations for its methods. It just sets up a contract for other classes to follow. Most languages begin the name of an interface with a capital "I" by convention, so we do that here as well. Now, we can make adapter classes for each of the separate APIs, and they will implement IPostsAPI, ensuring they all have a consistent interface to work with.

The adapter classes for the two post APIs follow:

import 'dart:convert';

class Site1Adapter implements IPostsAPI {
  final api = SiteApi1();

  List<Post>getPosts() {
    final rawPosts = jsonDecode(api.getSite1Posts()) as List;

    return rawPosts.map((post) =>
        Post(post['headline'], post['text'])).toList();
  }
}

class Site2Adapter implements IPostsAPI {
  final api = SiteApi2();

  List<Post>getPosts() {
    final rawPosts = jsonDecode(api.getSite2Posts()) as List;

    return rawPosts.map((post) =>
        Post(post['header'], post['body'])).toList();
  }
}

Like the rectangle adapter in the previous section, these adapter classes wrap the two different post APIs to change the way client code interacts with them. In this case, they provide access to the post content by implementing IPostsAPI, which obligates them to define a getPosts() method with a signature that matches the interface. Each adapter encapsulates its respective API and calls the appropriate API-specific acquisition method in its implementation of getPosts(). JSON posts are converted into Post model objects with the map() method and returned. Since the map() method resolves into a lazy Iterable, we need to call toList() to convert that into the List<Post> that getPosts() must return. It's necessary to import dart:convert to gain access to the jsonDecode() function.

Finally, the payoff. All this pattern work lets us get posts from any of the APIs without worrying about their differences:

final IPostsAPI api1 = Site1Adapter();
final IPostsAPI api2 = Site2Adapter();

final List<Post> posts = api1.getPosts() + api2.getPosts();

Since both adapter classes are guaranteed to have the same interface as IPostsAPI, we can type API variables as IPostsAPI. Now it's easy for client code to retrieve posts from all the APIs without worrying about the details of each, and they'll always return a list of Post models suitable for use in the app. This code gets posts from both APIs using the same method call, then concatenates them into a single list using the + operator.

Conclusion

The Adapter pattern is one of the most common and useful of the standard structural design patterns. It can be used to create a consistent interface across multiple differing APIs or to wrap an object with a less desirable interface to make it more compatible or convenient for client code. The pattern can be especially handy when your app must interact with APIs over which you have no control.

To read more about structural design patterns in Dart, check out these related articles: