Arrow icon
Effective Software Development using OpenAPI Generator Part 2

Effective Software Development using OpenAPI Generator Part 2

Apexive is a boutique software studio that builds tech products for startups in record time using the latest tech stacks.

Ajil Oommen

Senior Flutter Developer

August 8, 2023
Technology
Table of content

In the previous article, we covered the basics of working with OpenAPI generator to simplify the process of integrating APIs into front-end code.

This article aims to take it a step ahead by using OpenAPI Repository Annotations to simplify the process further. The idea behind creating this generator is to reduce the boilerplate code involved in setting up repositories, ListBlocs, etc., that use the generated openAPI code.

The Objective

Picking up from the previous article, we already have a basic setup for our generated client-side code in a package called `openapi`. To effectively use the generated code, we have to create our own Data repositories, Filters, ListBlocs, etc.

Recommended reading: Effective Software Development using OpenAPI Generator Part 2

But this process is repetitive and cumbersome. We also have to manually rewrite the code every time the API changes and the client-side code is generated. Besides, every time we write code, we run the risk of injecting new bugs into the system, which adds additional technical and monetary costs to our process.

While using the OpenAPI generator across several projects, we saw that the process of writing the Blocs, repositories, and filters, always followed the same pattern. We quickly set out to create our code generator that let us auto-generate the Blocs and Repositories. It saves us a lot of time and manual effort.

Setup

Generate OpenAPI Client side code

Follow steps from Effective Software Development using OpenAPI Generator Part 1 to generate the client side code to a flutter project called `openapi`

Create new Flutter App

We also need a base project, which can use the generated `openapi` dart project and connect to the `petstore_api` server.

1. Create a new flutter project using



flutter create petstore_app

2. In the `pubspec.yaml` for `petstore_app` add the following dependencies



dependencies:
      openapi:
        
      openapi_repository_annotations: 
      flutter_bloc: ^8.0.1
      dio: 
      list_bloc: 
      flutter_list_bloc: 
      freezed_annotations: 
      built_collection: ^5.1.1

3. Add the following to `dev_dependencies`




dev_dependencies:
      openapi_repository: 
      build_runner: ^2.1.11
      freezed: ^2.0.3+1
      json_serializable: ^6.2.0
     

4. Run `flutter packages get` to fetch all dependencies.

Usage

The primary focus of this article is to demonstrate how we utilize the power of code generation to replace manual effort. In this particular use case, we are using the following code generators:

1. `freezed`: Generates freezed classes that act as data classes for Filters used in the API. Coupled with the json_serializable generator, it provides powerful JSON serialization/deserialization capabilities.
2. `openapi_repository`: Generates the ApiRepository, ListBlocs, DataBlocs, Filters, and Repositories.

Creating the data layer

Inside `lib` create a folder called `data` and add a file called `api_repository.dart` inside it.

Inside `api_repository.dart` add the following code block:


import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:openapi/openapi.dart';
import 'package:openapi_repository_annotations/openapi_repository_annotations.dart';
import 'package:dio/dio.dart';
import 'package:list_bloc/list_bloc.dart';
import 'package:built_collection/built_collection.dart';

part 'api_repository.openapi.dart';
part 'api_repository.freezed.dart';
part 'api_repository.g.dart';

@OpenapiRepository(
  buildFor: Openapi,
  builderList: [
    RepositoryBuilder(PetApi, ignoreEndpoints: [
      'findPetByTagList',
    ]),
    RepositoryBuilder(StoreApi),
    RepositoryBuilder(UserApi),
  ],
  liveBasePath: r'https://petstore.swagger.io/v2',
  baseUrl: r'https://petstore.swagger.io/v2',
  dioInterceptor: Interceptor,
  defaultPageSize: 100,
  defaultOffset: 0,
  sendTimeout: 3000,
  connectTimeout: 3000,
  receiveTimeout: 5000,
)
abstract class $ApiRepository {}

The core part to note here is the `@OpenapiRepository` annotation which tells the @OpenapiRepository which parts it needs to build.

  • `buildFor`: The OpenAPI client for which the repository needs to be generated.
  • `builderList`: Specify here which methods/classes need to be used/ignored for generation.
  • `dioInterceptor`: A custom DioInterceptor instance

Generating the code

flutter packages run build_runner build --delete-conflicting-outputs

This will generate the following files in the same directory:

  • api_repository.freezed.dart
  • api_repository.g.dart
  • api_repository.openapi.dart

api_repository.openapi.dart

This file contains the core files generated by the annotation.

ApiRepository


class ApiRepository {
  static ApiRepository get instance => _instance;
  static final ApiRepository _instance = ApiRepository._internal();

  ApiRepository._internal() {
    _openapi.dio.options
      ..baseUrl = 'https://petstore.swagger.io/v2'
      ..connectTimeout = 3000
      ..receiveTimeout = 5000
      ..sendTimeout = 3000;
    _openapi.dio.interceptors.add(Interceptor());
  }

  static const String liveBasePath = 'https://petstore.swagger.io/v2';

  static final Openapi _openapi = Openapi(
    basePathOverride: kReleaseMode ? liveBasePath : null,
    interceptors: [],
  );

  Openapi get api => _openapi;
  PetApi get pet => api.getPetApi();
  StoreApi get store => api.getStoreApi();
  UserApi get user => api.getUserApi();
}

The `ApiRepository` class is generated with the parameters passed on to the annotation. This is modeled as a singleton instance which can be used to access the generated openAPI client code.

Data/List BLoCs

DataBlocs or ListBlocs are generated for each endpoint and operation, making it easier to call the API and maintain the state.

As an example for the /pets/:id GET endpoint, the following code blocks are generated

PetFilters

Filter classes are created for each operation that takes path or query parameters. This helps define a type-safe mechanism to pass parameters to the API in a single object, improving readability and reusability.

In this example, the filter class only has one parameter called petId for the filter:


//Filter for PetReadFilter

@freezed
class PetReadFilter with _$PetReadFilter {
  const PetReadFilter._();

  const factory PetReadFilter({
    required int petId,
  }) = _PetReadFilter;

  factory PetReadFilter.fromJson(
    Map map,
  ) =>
      _$PetReadFilterFromJson(map);
}

PetRepository

The repository classes for the base of the data layer as this is where different operations like read, create, update, delete, etc, are defined. The repository talks to the generated client side code in `openapi` through the `ApiRepository` singleton defined above.

The different operations in the Repository are defined based on the endpoints. If some endpoints or operations are ignored in the `@OpenapiRepository` annotation, those operations won't be generated here.


// Repository for PetRepository

abstract class PetRepository {
  static Future read([
    PetReadFilter? filter,
  ]) async {
    if (filter == null) {
      throw Exception('Invalid filter');
    }
    final r = await ApiRepository.instance.pet.petRead(
      petId: filter.petId,
    );
    if (r.data == null) {
      throw Exception('Failed to load data!');
    } else {
      return r.data!;
    }
  }

  Future create({
    required Pet body,
  }) async {
    final r = (await ApiRepository.instance.pet.petCreate(
      body: body,
    ));

    return r.data;
  }

  Future updateObject({
    required Pet body,
  }) async {
    final r = (await ApiRepository.instance.pet.petUpdate(
      body: body,
    ));

    return r.data;
  }

  Future delete({
    required int petId,
    String? apiKey,
  }) async {
    final r = (await ApiRepository.instance.pet.petDelete(
      petId: petId,
      apiKey: apiKey,
    ));

    return r.data;
  }
}

PetDataBloc

This is the Bloc layer which is the middleman between the presentation and data layer. Updates to the data in the Bloc will trigger UI updates in the presentation layer.

Similar to the Repository, the code is generated only for the allowed operations and endpoints:


// DataCubit for Pet

class PetDataBloc extends DataCubit with PetRepository {
  PetDataBloc(
    Future Function([
      PetReadFilter? filter,
    ])
        loader,
  ) : super(PetRepository.read);

  @override
  Future create({
    required Pet body,
  }) async {
    final r = await super.create(
      body: body,
    );

    return r;
  }

  @override
  Future updateObject({
    required Pet body,
  }) async {
    final r = await super.updateObject(
      body: body,
    );
    await super.load(state.filter);

    return r;
  }

  @override
  Future delete({
    required int petId,
    String? apiKey,
  }) async {
    final r = await super.delete(
      petId: petId,
      apiKey: apiKey,
    );

    return r;
  }
}

Integration

Once we have all the boilerplate code out of our way, implementing the front end logic is very straight forward:


DataBlocBuilder(
  cubit: bloc,
  emptyBuilder: (_, __) => const Center(child: Text('No data')),
  loadingBuilder: (_, __) => const Center(
    child: CircularProgressIndicator(),
  ),
  itemBuilder: (context, state) => ListTile(
    key: ValueKey(state.data?.id),
    onTap: () {
      if (state.data != null && state.data!.id != null) {
        Navigator.of(context).push(
          MaterialPageRoute(
            builder: (context) => PetDetailsScreen(
              petId: state.data!.id!,
            ),
          ),
        );
      }
    },
    title: Text(
      'id: ${state.data != null ? state.data!.id : 'No data'}',
    ),
    subtitle: Text(
      'Name: ${state.data != null ? state.data!.name : 'No data'}',
    ),
  ),
  builder: (context, state, item) => item(context),
)

Do have a look at our example project using this generator to have a better idea about the process.

Conclusion

We’ve learned what is OpenAPI Generator, what are the most effective ways to use OpenAPI Generator and how we can simplify development even more by using OpenAPI Repository Annotations.


I hope this article was useful to you and if you enjoyed it don't forget to share it.

At Apexive, we are on the lookout for talented engineers and technical people to help us build the amazing products for the startups! If you are interested in finding out more, check out our career page  Careers at Apexive.

Game Changers Unite

We are the software development agency and we love helping mission-driven startups in turning their ideas into amazing products with minimal effort and time.

LET'S TALK