DEV Community

Jan Mewes
Jan Mewes

Posted on

Calling REST APIs from Dart and Flutter

When using API-based services, they often bring an SDK for some of the most popular languages, to facilitate the creation of apps using their API (e.g. Supabase or Google Ads). This blog post describes a concept for building your own SDK for consumption of any REST API, using the Dart programming language for use in an app using the Flutter framework.


Table of contents


Terminology

The following table gives an overview of the abbreviations and technical terms used in the blog post.

Term Description
API Application Programming Interface; an interface of a software system that allows other software systems to use it.
SDK Software Development Kit; a library that provides access to a third-party service for the respective programming language.
REST API Short form for RESTful API which denotes a software architecture style based "representational state transfer".
KSCH Kirpal Sagar Charitable Hospital; the target domain of the app to be built.
Workflow A semi-automated business process.

Context

The backend service of the KSCH Workflows system provides a REST API for all the queries and commands which are needed for the apps in the KSCH workflows. The KSCH Dart Client facilitates it for all apps which use the Dart programming language to use that API by wrapping the low-level REST API with a high-level Dart API. The apps - e.g. the Registration desk, Pharmacy desk, and Administration app - can then use the KSCH Dart Client like any other Dart package.

Context diagram

In the text below, the KSCH Dart Client will also be referred to as "the SDK".

Usage

Before the discussion of the internal structure of the KSCH Dart Client, it will be shown how to use it.

Package dependency declaration

The KSCH Dart Client is a project-specific library that is of no use for the general Dart community. That's why the package is not published to the Dart package repository but is leveraging Dart's capability to declare dependencies directly on GitHub repositories.

dependencies:
  ksch_dart_client:
    git:
      url: https://github.com/ksch-workflows/ksch_dart_client
      ref: a85c7f0cb83087d13e207b1331bcf6f64676e995
Enter fullscreen mode Exit fullscreen mode

Once the system is in production use, semantic versioning tags should be used instead of specific commit IDs.

Instantiate API entry point

The next step in using the SDK is to create an instance of the KschApi class. This class has a parameter for the base URL of the backend service so that it can be configured to be executed against the testing or production system.

KschApi api = KschApi('http://localhost:8080');
Enter fullscreen mode Exit fullscreen mode

Collection resources

The first request to be done with the API is listing all patients with a GET request on the /patients collection resource.

http GET /patients
Enter fullscreen mode Exit fullscreen mode

The basic idea of the SDK is that it provides a property in the api object for every supported resource. So, in the case of this example, there is a patients property. On its value is a method available for each operation that can be done on that resource. By calling this method, the HTTP call is triggered and delivers its response wrapped in a Future.

await api.patients.list();
Enter fullscreen mode Exit fullscreen mode

Since collection resources are usually paged, the list method can take the index of the page to be requested as an optional parameter.

late PatientsResponsePayload response;
var page = 0;
do {
  response = await api.patients.list(page: page++);
  for (var patient in response.patients) {
    print(patient.id);
  }
} while (response.hasNextPage);
Enter fullscreen mode Exit fullscreen mode

Singleton resources

For working with individual patients, the singleton resource can be used.

http GET /patients/${PATIENT_ID}
Enter fullscreen mode Exit fullscreen mode

With the help of Dart's call method convention it is possible to invoke an object as a function. With this tool, a parameter can be passed to a collection resource for fluent access on a singleton resource below.

var response = await api.patients(patientId).get();
print(response.id);
print(response.name);
print(response.residentialAddress);
Enter fullscreen mode Exit fullscreen mode

Each resource may also have sub-resources. All resource path elements act as a builder and the HTTP call is triggered only in the final method call.

await api.patients(patientId).visits.startVisit(VisitType.OPD);
Enter fullscreen mode Exit fullscreen mode

Error handling

When there is an error response from the API, i.e. a status code >= 400, then an exception will be raised.

Usage in Flutter project

To avoid any HTTP-related ideas and too much business logic within the frontend widgets, the SDK is wrapped in a service layer. For every service, there is an interface which is widgets uses. During application start the main.dart file decides which service implementation should be used. Only that service implementation knows about the SDK and how to use it.

Service structure

The SDK can now be used in any Dart project. To spare the Flutter code of dealing with the request details, the SDK can be wrapped into service classes.

class PatientServiceImpl implements PatientService {
  final KschApi _api;

  PatientServiceImpl(KschApi api) : _api = api;

  @override
  Future<Patient> get(String patientId) async {
    var response = await _api.patients(patientId).get();
    return Patient.from(response);
  }
}
Enter fullscreen mode Exit fullscreen mode

The service can then be injected into the widget using e.g. get_it package.

class _RegisterPatientPageState extends State<RegisterPatientPage> {
  final PatientService patientService = GetIt.I<PatientService>();

  @override
  Widget build(BuildContext context) {
    return WebScaffold(
      title: 'Register patient',
      // ...
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          // ...
          final RegisterPatientResult? result = await showDialog(
              //...
              );
          if (result != null) {
            final createdPatient = await patientService.create(result.patient);
            // ...
          }
        },
        // ...
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Testing

Service tests

The service classes can be tested with the help of the nock package.

void main() {
  late PatientServiceImpl patientService;

  setUpAll(nock.init);

  setUp(() {
    nock.cleanAll();

    patientService = PatientServiceImpl('http://localhost');
  });

  test('Should create patient in case of emergency', () async {
    var patientId = const Uuid().v4();
    givenCreatePatientResponse(MinimalPatientResponse(patientId).toJson());

    var result = await patientService.create(null);

    expect(result.id, equals(patientId));
  });
}

void givenCreatePatientResponse(dynamic body) {
  nock('http://localhost').post('/api/patients')..reply(200, body);
}
Enter fullscreen mode Exit fullscreen mode

Widget tests

For the unit tests of the Flutter widgets, it should be fairly simple to create mocks for the used services.

Implementation details

Next comes a description of how the SDK is working internally.

Overview

The KSCH Dart Client as a small core with the KschApi class and model classes to be able to interpret pagination and links in the response payloads. The KschApi API provides an API for basic HTTP operations which can then be used by the resources. Further, it provides properties to the root resources.

The resources themselves are a tree structure, i.e. every resource can have subresources. Further, they know about their respective path element. Just before the actual HTTP called on the absolutePath, all the path elements of all ancestors are joined together.

The bases classes for the collection resources and identity resources are almost the same, with the difference that
identity resources know about their identification number.

ksch-dart-client-uml

Payload representations

The KSCH API uses JSON as the data format for request and response payloads. The code which does the mapping between JSON strings and Dart data types is generated with the help of the json_annotation library.

The Dart data types are a model of the JSON data structures. By default, the JSON properties are mapped with the Dart properties. If this is not possible, e.g. because a property in the JSON data starts with an underscore, then the mapping can be configured with custom annotation.

@JsonSerializable()
class VisitResponsePayload {
  @JsonKey(name: '_id')
  final String id;

  final VisitType type;

  final String opdNumber;

  final DateTime timeStart;

  VisitResponsePayload({
    required this.id,
    required this.type,
    required this.opdNumber,
    required this.timeStart,
  });

  factory VisitResponsePayload.fromJson(Map<String, dynamic> json) =>
      _$VisitResponsePayloadFromJson(json);

  Map<String, dynamic> toJson() => _$VisitResponsePayloadToJson(this);
}
Enter fullscreen mode Exit fullscreen mode

The details of the fromJson and toJson methods are then generated from the Dart build runner.

dart run build_runner build
Enter fullscreen mode Exit fullscreen mode

Tests

The unit tests for the SDK act also as API tests for the backend API. The expectation is that the backend has been locally started on port 8080. Then they are using the real API to make sure that all requests can be successfully executed and all responses successfully parsed. Later on, this test suite will also be included in the build process of the backend to make sure that no unintended breaking API changes are made.

Discussion (0)