How to Implement a Clean Service Layer in Flutter (With API Examples)

Jimmy Patterson API Development, Development Technologies & Tools, Flutter, Mobile Leave a Comment

Flutter is a powerful framework for building mobile apps (among other things) that look great, perform well, and scale fast—without relying on platform-specific knowledge, superhuman ability, or heading too deep into your team’s crunch time. Even so, any large codebase maintained over a long time will incur their own mountain of tech debt as versions update, business needs change, and the inevitable list of bugs begins to form.

One of the most effective ways to keep your Flutter applications maintainable is by implementing a clean, efficient service layer. This blog post outlines an efficient example implementation of the Service Layer of a Flutter Mobile app—specifically building a service layer in Flutter, showing how to structure code for calling external APIs, and abstracting logic in a way that makes switching data sources simple.

If you’re better served by coding experience than merely reading along, there is an accompanying example project you can follow through, where we implement the service layer to call third-party APIs. You can find the example project here and follow along with the branches prefixed with api to see my code implementation.

What Is The Service Layer?

There are plenty of ways to cut an application into sections, and depending on your needs, your definitions of each section might vary. So, for the purposes of this demonstration, here’s what we will classify as a “service layer.” When you need to interact with something outside of the application, I consider that to be interfacing with a service.

In Flutter applications, the service layer acts as the bridge between your app and external systems. Whenever your app interacts with something outside its own codebase, you’re working with a service.

There are some examples of this which are probably pretty gray in terms of fitting the Service Layer “label,” so there’s some leeway, but some examples that I consider to be rock-solid.

  • Interacting with your app’s backend API
  • Interacting with a third-party through their API
  • Interacting with local storage at the file or LocalDB level
  • Interacting with specific hardware- for instance, using Bluetooth connection

Obviously, these things are pretty different, and not all of them fit into the capital-S “Service” label, but for the purposes of easy and maintainable mobile development, I would want to treat them all pretty similarly, as a service.

Flutter Implementation Gameplan

We’ll start with a simple Flutter app (with the default counter removed), then build a service that fetches data from a third-party API and displays it on the home page. Then, we will pivot to using a second third-party API, and see how abstracting the calls away makes it easy to transition between third-party APIs. Through swapping APIs, you’ll be able to see how abstraction makes changes painless.

First Service Example: Holiday API

First, we should make a folder to contain files related to a service. For now, we can just create the file CoolThingService.dart inside a folder called services inside the lib directory.

We will use the CoolThingService functions to call the free Holiday API at date.nager.at. This has some implications on the main screen, namely that these holidays don’t have an associated id field, so there are a few changes to make to the CoolThing model, namely switching out the id field for a more aptly titled subtitle field, since the thing gets shown as a title and subtitle in a card on the home screen.

Then, on the Home Screen itself, we adjust the initial CoolThing to use a subtitle instead of an ID, and add a button to call the service. Oh, and we need to add the http dependency to our pubspec.yaml file, as well.

The really important stuff is handled in the service itself:

  // ignore: constant_identifier_names
  static const String BASE_URL = "https://date.nager.at/api/v3/";

First, we define the base URL, so every API request within this service is built off the same URL. Then, we import the http package from our dependencies and call the endpoint we want to call.

    http.Response response = await http.get(
      Uri.parse("${BASE_URL}NextPublicHolidays/$input"),
    );

Finally, we can parse the response, with its JSON formatting, and create a new instance of the model to return.

 List<dynamic> responseList = json.decode(response.body) as List<dynamic>;
 Map<String, dynamic> randomHoliday =
          (responseList)[Random.secure().nextInt(responseList.length)];


 return CoolThing(
    name: randomHoliday['localName'],
    subtitle: randomHoliday['date'],
 );

Back on the Home Screen, we simply need to call the service when the button is clicked and then update the state when we receive a successful response.

    CoolThing? foundCoolThing = await CoolThingService()
                   .getSingleThing("US");
    if (foundCoolThing != null) {
       setState(() {
         thing = foundCoolThing;
       });
    }

Switching to a New Third-Party API: Gutendex

Now, let’s imagine that there was a change in the business, or your boss decided holidays were no longer cool, or for some reason at all, the new “Cool Thing” is books. Now, it’s a new imperative to use a different API for the app- the Gutendex.

To prepare for the next pivot, we will implement two abstract classes to ensure smooth operation. We want to display a Cool Thing, so we can make that class abstract, and we also want to make the whole service an abstract class. Then, we make a HolidayService class to implement the abstractions from the CoolThingService, and make a Holiday class to implement the CoolThing abstract class. From there, we can still run the old code and get good Holiday-flavored responses.

(Note: There may be a few syntax changes to make on the Home screen and such in order to adjust to the new names of classes!)

Now that the concept of the Cool Thing has been abstracted, we can add a new service to call the new API endpoint, which is BookService and the Book class. Just for kicks, we will also be tracking the download count for the Book objects, just to identify a way in which the Book object is distinct from a typical CoolThing.

class Book extends CoolThing {
  final int downloads;


  const Book({
    required super.name,
    required super.subtitle,
    required this.downloads,
  });
}

For the BookService, the structure looks awfully similar to HolidayService, with a little bit of notable tweaking to the URL we use for the GET request.

class BookService extends CoolThingService {
  @override
  String get BASE_URL => "https://gutendex.com/books/";


  @override
  Future<CoolThing?> getSingleThing(String input) async {
    http.Response response = await http.get(Uri.parse("$BASE_URL?id=$input"));


    if (response.statusCode >= 200 && response.statusCode < 300) {
      List<dynamic> responseList =
          (json.decode(response.body)['results']) as List<dynamic>;
      Map<String, dynamic> randomHoliday =
          (responseList)[Random.secure().nextInt(responseList.length)];


      return Book(
        name: randomHoliday['title'],
        subtitle: randomHoliday['media_type'],
        downloads: randomHoliday['download_count'],
      );
    }
    return null;
  }
}

And with those pieces in place, as well as shifting the HomeScreen to use the BookService, we can see that the other API is called and a CoolThing is correctly parsed each time!

Enhancing The Service Layer

So, there we have it – we’ve added a service layer to the basic app and made it resilient to third-party or model changes. From here, it won’t be tough to add better features.

But what sorts of other features should be added to this service layer, for instance?

Request and Response Classes

As more and more API calls are added to the underlying CoolThingService abstract class, the class will fill up with some redundant lines of code, especially the parsing of the HTTP Response objects, and turning into models.

Here are some methods that an abstract version of Request and Response should contain:

Request

String urlExtension
Map <String, dynamic> body
String toString, as more abstractions tend to lead to stuff getting lost in the weeds

Response

Response fromJson
String toString

HTTP Response Sanitation

Another way to try and cut down on extraneous lines in the CoolThingService class and the classes that implement it is to centralize the way an HTTP Response is verified and to condense the way errors are handled.

With the implementation of Request and Response classes as well, each API call in the service implementation class should look something like this:

  /// LOGIN
  @override
  Future<Login.Response> login(Login.Request request) async {
    http.Response response = await http.post(
      Uri.parse("${Env.BASE_URL}${request.urlExtension}"),
      headers: defaultJsonHeaders,
      body: json.encode(request.body),
    );


    if (kDebugMode) Logger.info("Login response ${response.body}");
    bool success = checkErrors(
      "Unable to login",
      response,
    );


    return success
        ? Login.Response.fromJson(decode(response.body)['data'])
        : Login.Response.failure;
  }

(This code snippet has been ripped from a personal project, and has its identifying details removed.)

Conclusion

A mobile app is rarely complete without a service layer. In fact, most apps rely very heavily on the other services which provide data, access to hardware, or store user information. This means it’s really important to implement your service layer in a maintainable, adaptable, and testable way.

I hope this approach I’ve listed out above is one you can use to meet and exceed these goals. It’s built to be modular and scalable, and it’s easy to see how to shift this example when worrying about non-API interactions, like with a local database or piece of hardware.

Building a great service layer will help you stay ahead of your bugs and prepared for the worst case backend scenarios- even changing the entire backend- while keeping a hold of the pieces your app needs in order to build a great user experience. Hopefully following these steps here will also save you the initial headache of implementation!

For more expert guidance on building better mobile applications, visit the Keyhole Software blog.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments