Irina Southwell is a ClearPoint practice lead. In this blog, she shares her experience with Flutter and teaches you how to accelerate your development.

My adventure with Flutter started about 6 months ago.  It has been a really exciting time with a lot of learning and a sense that I am getting one big step ahead in the ever changing world of technology.

I’ve found that when thinking about mobile application development, we often  first consider user-facing screens, responsiveness, UX flow and so on. However, just as important for mobile and web applications are the back-end services, data, and business logic that needs to surface through the user interface (UI). Using REST requests to expose and fetch the data from the back-end APIs is one of the common techniques.

To achieve this with Flutter, in a traditional approach, we would use a library that can perform REST requests, e.g “http” package. Then we would process the response and feed it to the Flutter widgets to render the data from the response on screen.

For trivial applications, that can be an easy way to go. However, when some of my colleagues and I were working on more complex mobile apps that require communication with larger sets of REST APIs, we immediately realised that writing our own API clients and JSON deserializers is time consuming, prone to errors and may break when APIs evolve over time. It also takes time away from building the fun stuff, you want to make sure your team members don‘t end-up spending most of their time on maintaining and updating API clients. So, we thought of OpenAPI contracts which we already successfully used across multiple projects to build microservice based API’s.

“The OpenAPI Specification (formerly known as Swagger Specification) is an API description format for REST APIs. An OpenAPI file allows you to describe your entire API.”

With OpenAPI, you can have your REST API described in a single yaml or json file and then based on that file, generate the client and server code automatically without any manual intervention. Imagine how much time you save. Instead of writing your own client libraries, you generate the client code in seconds ready to be consumed by your Flutter app. In addition to this, there is no need to write deserializers as your responses are typed in accordance to models described in the Open API files.  You also don’t need serializers when sending data, instead you can use typed objects (models) to construct the request data.

After some research, we realised that built-in OpenApi code generation plugin for Dart wasn’t perfect for our needs. Thanks to Richard Vowles who forked from the original version and improved it with some key features that were missing:

  • Support for enums
  • Better support for cross-platform code generation (mobile / web)
  • Support for object equality checks

In this article I will describe how to use this plugin in order to advance development with Flutter for applications that use REST services to communicate with the back-end. The plugin itself can be found and downloaded from here https://search.maven.org/search?q=a:openapi-dart-generator

Tutorial

1.   Identify the requirements

Let’s imagine we are tasked to build a sample application “Counter” and we are required to communicate to a simple back-end service running on port 8076. The service can do the following operations:

  • Get current counter number
  • Increase counter by specified amount and
  • Reset counter

 

2.   Create Open API definition yaml file to describe the  back-end service operations

Make sure you are using Open API version 3.0.  There are a number of open source and commercial tools you can use to help you with this. Swagger Editor and Stoplight are a couple that can be used.

openapi: 3.0.1

info:
 title: CounterApiExample
 description: This is just an example of the counter app
 version: "1.1.1"

paths:
 /counter:
   get:
     tags:
       - CounterService
     description: "Get the current value of the counter"
     operationId: getCounter
     responses:
       "200":
         description: "the counter's current value"
         content:
           application/json:
             schema:
               $ref: "#/components/schemas/Counter"
   delete:
     tags:
       - CounterService
     description: "Reset the counter"
     operationId: resetCounter
     responses:
       "200":
         description: "the counter's current value"
         content:
           application/json:
             schema:
               $ref: "#/components/schemas/Counter"
   post:
     tags:
       - CounterService
     description: "Increase the current value of the counter by an amount"
     operationId: incCounter
     requestBody:
       required: true
       content:
         application/json:
           schema:
             $ref: "#/components/schemas/Counter"
     responses:
       "200":
         description: "the counters current value"
         content:
           application/json:
             schema:
               $ref: "#/components/schemas/Counter"

components:
 schemas:
   Counter:
     required:
       - amount
     properties:
       amount:
         type: integer

 

Once you’ve created your API spec, add it to your project folder. In my project, I have a folder called “api” where I store counter.yaml that describes a ‘Counter’’ API:

 

3.   Generate API client code in Dart using Codegen plugin

  • Install Java SDK. Can be downloaded from here

https://www.oracle.com/technetwork/java/javase/downloads/index.html#JDK11

(Both codegen plugin and openapi-generator are written in Java and therefore require Java SDK)

 

  • Download the codegen plugin

https://search.maven.org/search?q=a:openapi-dart-generator current version: 2.3

 

  • Download OpenAPI CLI (group id: org.openapitools)

https://search.maven.org/search?q=a:openapi-generator-cli current version: 4.2.1

 

  • From the folder where you downloaded the above run the following command:
java -cp openapi-generator-cli-4.2.1.jar:openapi-dart-generator-2.3.jar org.openapitools.codegen.OpenAPIGenerator generate -i ../api/src/main/resources/counter.yaml --additional-properties pubName=counterapi -g dart2-api --enable-post-process-file

 

Alternatively if you have Maven installed you can run the following command:

mvn clean generate-sources

 

And watch the code generation in progress…!

 

Once completed – you should be able to see api client code, models and pubspec yaml file generated in the same folder:

  • Download Dart dependencies

You can see that some classes have unresolved imports, so run pub get command from the same folder to get the dependencies

 

That was pretty easy, in 3 simple steps we have Dart client code and API models generated and ready to be used in our Flutter app.

 

4.   Reference the generated library in the Flutter sample app

First, let’s reference the library in the Flutter sample app pubspec.yaml by providing a path to counterapi (folder name where you generated the code from)

dependencies:
 flutter:
   sdk: flutter
 rxdart: 0.22.0
 counterapi:
   path: ../counter_api

 

Then run the following command to install dependencies:

pub get

 

5.   Time to write some code and fetch the data!

Everything should be ready now for using the api calls in the sample app.

  • Let’s create client proxy class which will help us to instantiate API calls in Flutter BLOCs. Create client.dart file and add the following code, including the path to your back-end service:
import 'package:openapi_dart_common/openapi.dart';

import 'package:counterapi/api.dart';

class Client {
 ApiClient _apiClient;
 CounterServiceApi _counterServiceApi;
 CounterServiceApi get counterServiceApi => _counterServiceApi;

 Client() {
   _apiClient = ApiClient(
       basePath: "http://localhost:8076",
       deserializeDelegate: LocalApiClient());
   _counterServiceApi = CounterServiceApi(_apiClient);
 }
}
  • Now I can create counter_bloc.dart. In this class I will take care of the business logic following BLOC pattern and write some code to make actual API calls to the “Counter” service. Since one of the requirements is to get current “counter” value, I am going to create such method. But before this let’s make sure we have an instance of the Client in the constructor of this class:
class CounterBloc {
 final Client client;

 //constructor
 CounterBloc(this.client) { }

Now I can create the getCounter() method:

Future<Counter> getCounter() async {
 return client.counterServiceApi.getCounter();
}

As you can see instead of writing my own client code to deal with this GET request, I just used the generated getCounter() method. In addition to this I also get typed response, in this case – “Counter” object , so there is no need to deserialize json response.  To get the counter int value we can simply do _counter.amount

IntelliJ also gives me handy hints on which object properties I can access

 

 

 

 

 

  • But this method doesn’t do much yet. In the next step, let’s also start using streams so in the future a Flutter widget can listen to the stream events and update on the fly when the counter value changes. I will also create a couple more methods to “update the counter” and “reset the counter”.
class CounterBloc {
 final Client client;

 Counter _counter;
 final _counterStream = rxdart.BehaviorSubject<Counter>();
 Stream<Counter> get counter => _counterStream.stream;

 //constructor
 CounterBloc(this.client) {
   initialise();
 }

 void initialise() async {
   await getCounter();
 }

 void dispose() {
   _counterStream.close();
 }

 getCounter() async {
   _counter = await client.counterServiceApi.getCounter();
   if (_counter == null) {
     _counter = new Counter()..amount = 0;
   }

   _counterStream.add(_counter);
 }

 counterInc() async {
   Counter cnt = new Counter()..amount = 1;
   _counter = await client.counterServiceApi.incCounter(cnt);
   _counterStream.add(_counter);
 }

 counterReset() async {
   _counter = await client.counterServiceApi.resetCounter();
   _counterStream.add(_counter);
 }
}

 

6.   Display the data on screen

I have the BLOC class constructed – let’s import it and use it in a Flutter widget to stream and display data on the front-end. I can also call methods from the counter_bloc.dart to “get”, “update” and “reset” the counter.

import 'package:counterapi/api.dart';
import 'package:example_mobile_client/client.dart';
import 'package:flutter/material.dart';
import 'counter_bloc.dart';

void main() {
 runApp(MyApp());
}

class MyApp extends StatelessWidget {
 final client = Client();

 @override
 Widget build(BuildContext context) {
   final _bloc = CounterBloc(client);

   return CounterBlocContext(
       child: MaterialApp(
         title: 'Counter App',
       home: MyHomePage(title: 'Counter App Home Page'),
       ),
       bloc: _bloc);
 }
}

class MyHomePage extends StatelessWidget {
 final String title;

 MyHomePage({Key key, this.title}) : super(key: key);

 @override
 Widget build(BuildContext context) {
   var bloc = CounterBlocContext.of(context).bloc;
   return Scaffold(
     appBar: AppBar(
       title: Text(this.title),
     ),
     body: Center(
         child: StreamBuilder(
             stream: bloc.counter,
             builder: (BuildContext context, AsyncSnapshot<Counter> snapshot) {
               return Column(
                   mainAxisAlignment: MainAxisAlignment.center,
                   children: <Widget>[
                     Text(
                       'You have pushed the button this many times:',
                     ),
                     Text(
                       '${snapshot.data.amount.toString()}',
                       key: Key('counter'),
                       style: Theme.of(context).textTheme.display1,
                     )
                   ]);
             })),

     floatingActionButton: Row(
       mainAxisAlignment: MainAxisAlignment.end,
       children: <Widget>[
         FloatingActionButton(
             key: Key('increment'),
           onPressed: () => bloc.counterInc(),
           tooltip: 'Increment',
           child: Icon(Icons.add),
         ),
         SizedBox(width: 10),
         FloatingActionButton(
           key: Key('reset'),
           onPressed: () => bloc.counterReset(),
           tooltip: 'reset',
           child: Icon(Icons.refresh),
         ),
       ],
     ),
   );
 }
}

For the full example please refer to the GitHub repository.

It should be all set now and ready to run the app!


Run Flutter app and the back-end service example

If you like the tutorial above and would like to try a full example with our back-end server and the Flutter app you can clone this repo:

https://github.com/dart-ogurets/dart-full-api-ogurets-flutter

Follow the instructions to build and run the java server with Docker (Counter Api) and Flutter app on a mobile device.


Conclusion

You may be wondering if you should be using OpenAPI contracts and code generation plugins with Flutter. If you think one of the below points applies to you, it’s likely that you will benefit from using it:

  • You are trying to develop a complex app with Flutter and you have a bunch of microservices to talk to.
  • You realised that supporting all those “get” and “post” requests and writing your own json serializers/deserializers is fragile, risky and becoming a high maintenance task.
  • Inconsistent API server or client code in different applications across your tech stack and you are not sure where the source of truth is.
  • In test automation: You are testing back-end services or using them to create test data for your Flutter app. Writing your own API client libraries to handle HTTP requests may become a high maintenance task.

 

If you would like help with developing using Flutter or you need a beautiful app created in record time, please contact us.

See how we created the MyAMP iOS and Android apps in just 14 weeks here.

See how we built the Zip payments mobile app in under 3 months here.