KINTO Tech Blog
Development

The Best Practices Found by Backend Engineers While Developing Multiple Flutter Applications at Once

Cover Image for The Best Practices Found by Backend Engineers While Developing Multiple Flutter Applications at Once

Hello. I am Ohsugi from the Woven Payment Solution Development Group.

My team is developing the payment system used by Woven by Toyota for Toyota Woven City. We typically use Kotlin/Ktor for backend development and Flutter for the frontend. In a previous article, I discussed the process of selecting the frontend technology prior to the development of our web application. Since then, we have expanded our operations and are currently working on seven Flutter applications, including both web and mobile platforms.

In this article, I will talk about how our team, which had only backend engineers, came up with ways to efficiently develop multiple Flutter applications in parallel with backend development.

Flutter and the related logo are trademarks of Google LLC. We are not endorsed by or affiliated with Google LLC.

Overview

As mentioned, our team does backend and frontend development for a payment system. It will be a payment application to be used by people, accessible through either a web-based management interface or a Proof of Concept (PoC) on mobile devices at Woven City. In order to develop the Flutter app efficiently in parallel with the backend development of the payment system, we decided to take the following steps.

  • Design a Common Application Architecture
  • Form a Design Policy for Lazy UI Components
  • Define the Tech Stack Unification and Development Flow

Design a Common Application Architecture

There were various proposals for both backend and frontend architectures over the years, but I think it is best to pick one that suits the development team and product phase and improve along the way. We have adopted a clean architecture for backend development and applied an architecture for Flutter applications that uses only MVVM and repository patterns with a similar layer-first directory structure. To be specific, the directory structure is as follows.

Directory Configuration

lib/
├── presentations
│   ├── pages
│   │   └── home_page
│   │       ├── home_page.dart
│   │       ├── home_page_vm.dart
│   │       ├── home_page_state.dart
│   │       └── components
│   ├── components
│   ├── style.dart // a common style definition
│   └── app.dart
├── domains
│   ├── entities
│   └── repositories // repository interface
├── infrastructures
│   └── repositories
└── main.dart

Directory Roles

There are three main directories that make up the layer structure. Here are some brief descriptions of each directory's role.

Directory Layer Role
presentations Presentation Layer Defines View, ViewModel, and if necessary, the states
domains Domain Layer Defines interfaces for domain models, logic, and repositories
infrastructures Infrastructure Layer Defines repository tier implementations, including those for API calls

When designing with a layer pattern, you may want a use case layer, but there is currently very little business logic in the frontend, so we have included it in ViewModel.

The application we are developing does not have complex functions yet, and is basically page = one domain, so we are proceeding smoothly with this design. When creating a new app with a PoC, we usually start with this template so that there are no differences in architecture between applications.

Form a Design Policy for Lazy UI Components

When designing UI components, we decided not to adopt Atomic Design and to not make too many common components. There are some drawbacks, but we did it this way for the for the following reasons:

  • It was difficult for all members to get the same sense of the levels in the Atomic Design classifications
  • We wanted to focus on page implementation rather than making common components
  • Most importantly, it takes a lot of energy to build an abstract widget with Flutter

I think that making common components is more common, but at this point in time, we are in the phase of making changes to the application while flexibly changing specifications, and we decided that it would be more beneficial to not standardize it much in the short term.

Define the Tech Stack Unification and Development Flow

Many different technologies for state management and screen transition frameworks have come and gone. Beginners get confused about which library to use because there is so much information. I have experienced it as well and can relate. So we decided to use the following tech stack across all applications.

Tech Stack

Target Library
State management and provider creation riverpod
Model definition freezed
Screen transition go_router
API client dio, openapi-genrator
Project management melos

:::message We are doing schema-driven development using OpenAPI and automatically generate frontend API clients using openapi-generator based on the OpenAPI schema yaml file created in during backend development. :::

We use Riverpod for the state management and provider creation. Riverpod's concept of providers is not well-suited for backend development, and since it is possible to implement the provider as you like by coding by hand, the implementation flow and application location are defined somewhat strictly.

  1. Make sure to use riverpod_generator to generate the provider
  2. The provider is used when binding the infrastructure layer repository to the domain layer interface

Future<HogeRepository> hogeRepository(HogeRepositoryRef ref) {
  final apiClient = await ref.watch(openApiClientProvider);
  return HogeRepositoryImpl(
    apiClient: apiClient.getHogeApi(),
  );
}
  1. The ViewModel is implemented with AsyncNotifierProvider, and the provider of the repository required by View is aggregated into ViewModel

class HogePageViewModel extends _$HogePageViewModel {
  
  
  Future<List<Hoge>> build() {
    return _fetchData();
  }

  Future<List<Hoge>> _fetchData() {
    repository = ref.watch(hogeRepositoryProvider);
    return repository.getList();
  }
  
  Future<void> registerData(Hoge hoge) {
    repository = ref.watch(hogeRepositoryProvider);
    return repository.register(hoge);
  }
}
  1. View monitors the AsyncValue from the ViewModel and displays the UI. Alternatively, CRUD is processed to the repository via ViewModel

As mentioned above, we defined the process from repository implementation to embedding the UI and backend. Tickets are divided by particle size according to the process when making sprint tasks.

Conclusion

As the client application development priority rose in the project, we established a frontend development policy and came up with ways to develop smoothly as a team. Since many web management screens have a basic set with a list screen, details screen, and editing screen, we are also thinking about implementing measures to implement UI more efficiently using code generators in the future.

Facebook

関連記事 | Related Posts

We are hiring!

【Toyota Woven City決済プラットフォームフロントエンドエンジニア(Web/Mobile)】/Toyota Woven City Payment Solution開発G/東京

Toyota Woven City Payment Solution開発グループについて我々のグループはトヨタグループが取り組むToyota Woven Cityプロジェクトの一部として、街の中で利用される決済システムの構築を行います。Toyota Woven Cityは未来の生活を実験するためのテストコースとしての街です。

【Woven City決済プラットフォーム構築 PoC担当バックエンドエンジニア(シニアクラス)】/Toyota Woven City Payment Solution開発G/東京

Toyota Woven City Payment Solution開発グループについて私たちのグループはトヨタグループが取り組むWoven Cityプロジェクトの一部として、街の中で利用される決済システムの構築を行います。Woven Cityは未来の生活を実験するためのテストコースとしての街です。