KotlinエンジニアがFlutterに入門して1ヶ月でWebアプリケーションを作った話
KotlinエンジニアがFlutterに入門して1ヶ月でWebアプリケーションを作った話
こんにちは。Woven Payment Solution 開発グループの大杉です。
私たちのチームは、 Woven by Toyota において Toyota Woven City で使われる決済システムの開発を行っていて、普段はKotlin/Ktorによるサーバーサイドの開発をしています。
私たちは、Woven Cityを一緒に作っていく協力企業や社内のビジネスチームと協力してPoC (Proof of Concept, 概念実証)を繰り返し、 決済システムの機能を拡充させていっています。そして先日、第一弾として実店舗での小売販売を想定した決済システムのPoCを実施しました。
この記事では、私たちがPoCの中でクライアントアプリの開発にFlutterを採用するに至った経緯を紹介したいと思います。
はじめに
PoCで小売販売をするために、決済システムの他に次のような店舗運営向けの機能開発も行いました。
- 店舗で販売する商品の管理
- POSレジ
- 商品のスキャン
- ショッピングカート機能
- 店舗への売上報告と支払いの管理
- 棚卸し
特に、数万件に及ぶ商品情報の定期更新や月末の締日に合わせた売上報告、棚卸しを実施するためには、決済APIだけではなく、技術者ではない店舗担当者が作業できるGUIアプリケーションが必要でした。これが、普段はサーバーサイドの開発を行っている私たちが急遽クライアントアプリを開発に着手するに至ったきっかけです。
言語・フレームワークの選定
クライアントアプリを開発するに当たり、WebだけでなくiOS/Androidでもアプリ開発ができるクロスプラットフォームのフレームワークに候補を絞りました。
言語 / フレームワーク | 選定理由 |
---|---|
Dart / Flutter, Flutter on the web | - 最近注目されているトレンドな技術である - 社内のモバイルアプリ開発チームでも採用されているため、チーム間の親和性が高い |
TypeScript / Expo (React Native), Expo for web | - Web開発に関しては最も成熟した技術の一つであるReactで開発できる - Reactの開発経験のあるチームメンバーが多く、キャッチアップに時間がかからない |
Kotlin / Compose Multiplatform, Compose for web | - 採用事例がまだ少ないため、チャレンジングな開発ができる - 開発経験のあるチームメンバーはいないが、Kotlinを書ける人にとっては実装しやすそう |
技術検証
言語・フレームワークを選定するために、クライアントアプリを開発する上で大事な要素である状態管理と画面遷移を組み合わせたWebアプリを作成して技術検証を行いました。
作成したアプリは、+
-
ボタンを押下すると数値がカウントアップして、左の画面(Home Page)のnext
ボタンを押下すると右の画面(Detail Page)に遷移して数値を表示するというすごく単純なものです。
それぞれの言語・フレームワークの組み合わせについて、UIコンポーネントの実装方法、パフォーマンス、ライブラリ・ドキュメント・コミュニティサポートの点で開発体験の違いを見てみました。
UIコンポーネントの実装方法
まずは、上図右のDetail Pageのコードを例にFlutter on the web, Expo for web, Compose for webを比較してみました。
Dart / Flutter on the web
- DOMではなくオブジェクト指向なコンポーネントでUIを実装できるので、とても直感的だと感じています
- モバイルとWebでほとんど同じコードを利用できます
- スタイリングにはMaterial Designがデフォルトで適用されるので、一長一短ありますが、エンジニアがデザインもする必要がある状況ではとても助かります
- Canvaskitでレンダリングする場合、ほとんど同じ見た目のUIを描画することができます
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
Widget build(BuildContext context) {
final args =
ModalRoute.of(context)!.settings.arguments as DetailPageArguments;
return Scaffold(
appBar: AppBar(
title: const Text("Flutter Demo at Detail Page"),
),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 120),
child: Center(
child: Text(
args.value.toString(),
style: const TextStyle(fontSize: 72),
),
),
),
),
);
}
}
TypeScript / Expo
- Flutter同様に、DOMではなくオブジェクト指向なコンポーネントでUIを実装できるので、とても直感的だと感じています
- ただし、フレームワークが用意してくれるコンポーネントは最小限であるため、実装が必要です
- モバイルとWebでほとんど同じコードを利用できます
- スタイリングは、StyleSheetというCSSに似た記法でスタイリングしますが、適用されるスコープが限定されるためCSSほど辛くないです
- このサンプルの画面遷移の実装にはreact-navigationを使用しています
const DetailPage: React.FC = () => {
// from react-navigation
const route = useRoute<RouteProp<RootStackParamList, 'Detail'>>();
return (
<View>
<Header title={'Expo Demo at Detail Page'} />
<CenterLayout>
<Counter value={route.params.value}/>
</CenterLayout>
</View>
);
}
const Header : React.FC<{title: String}> = (props) => {
const {title} = props;
return (
<View style={styles.header}>
<Text style={styles.title}>
{title}
</Text>
</View>
)
}
const CenterLayout: React.FC<{children: React.ReactNode}> = (props) => {
const {children} = props;
return (
<View style={styles.layout}>
{children}
</View>
)
}
const Counter: React.FC<{value: number}> = (props) => {
const {value} = props;
return (
<View style={styles.counterLayout}>
<Text style={styles.counterLabel}>{value}</Text>
</View>
)
}
const styles = StyleSheet.create({
header: {
position: "absolute",
top: 0,
left: 0,
width: '100%',
backgroundColor: '#20232A',
padding: '24px 0',
},
title: {
color: '#61dafb',
textAlign: 'center',
},
layout: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
},
counterLayout: {
minWidth: 120,
textAlign: 'center'
},
counterLabel: {
fontSize: 72,
}
});
Kotlin / Compose for web
- モバイルやデスクトップで扱うCompose UIではなく、HTMLのDOMをラッパーしたようなWeb専用のコンポーネントでUIを実装します
- モバイルとWebでコードの流用はできません
- スタイリングは、CSSで実装する必要があります
- コンポーネントに対してCSSのようなプロパティをコンポーネントに直接定義するか、StyleSheetオブジェクトとして切り出して実装します
- このサンプルの画面遷移の実装にはCompose multiplatformのWebとデスクトップ向けのrouting-composeというライブラリを使用しています
@Composable
fun DetailPage(router: Router, params: Map<String, List<String>>?) {
Div {
components.Header(title = "Compose for web Demo at Detail Page")
CenterLayout {
params?.get("value")?.get(0)?.let { Counter(it.toInt()) }
}
}
}
@Composable
fun Header(title: String) {
H1(attrs = {
style {
position(Position.Fixed)
top(0.px)
left(0.px)
paddingTop(24.px)
paddingBottom(24.px)
backgroundColor(Color("#7F52FF"))
color(Color("#E8F0FE"))
textAlign("center")
width(100.percent)
}
}) {
Text(title)
}
}
@Composable
fun CenterLayout(content: @Composable () -> Unit) {
Div(attrs = {
style {
display(DisplayStyle.Flex)
flexDirection(FlexDirection.Row)
justifyContent(JustifyContent.Center)
alignItems(AlignItems.Center)
height(100.vh)
}
}) {
content()
}
}
@Composable
fun Counter(value: Int) {
Span(attrs = {
style {
minWidth(120.px)
textAlign("center")
fontSize(24.px)
}
}) {
Text(value.toString())
}
}
パフォーマンス
次に、それぞれの言語・フレームワークで作ったサンプルアプリに対してビルド時間とバンドルサイズの比較をしました。
ビルド時の最適化オプションはデフォルトのものを使用しています。
検証環境は、MacBook Pro 2021 (CPU: M1 Pro, Memory: 32GB)です。
言語 / フレームワーク | ビルド条件 | ビルド時間 | バンドルサイズ |
---|---|---|---|
Dart / Flutter on the web | - Flutter v3.7.7 - Dart v2.19.2 |
14s | 1.7MB (CanvasKit) 1.3MB (Html) |
TypeScript / Expo for web | - TypeScript v4.9.4 - Expo v48.0.11 |
10s | 500KB |
Kotlin / Compose for web | - Kotlin v1.8.10 | 9s | 350KB |
Flutterで同機能を提供するために必要なバンドルサイズはReactのおよそ10倍であり、初回レンダリングにはかなり時間がかかってしまう可能性が高いことがわかります。
Flutterで生成されるJSコードについては、ビルドオプションに--dump-info
を追加することで詳細を確認することができ、 主に、dartとFlutterのフレームワーク部分のコードが含まれていました。
ライブラリ・ドキュメント・コミュニティサポート
最後に、それぞれの言語・フレームワークについてライブラリ・ドキュメント・コミュニティサポートなどの情報をまとめました。
言語 / フレームワーク | ライブラリ | ドキュメント・コミュニティサポート |
---|---|---|
Dart / Flutter on the web | Flutter packagesでFlutterで利用可能なライブラリを検索できる。 その中でも Flutter Favorite が付いているものは公式が人気があり使いやすいライブラリであることを提示してくれている。 |
公式ドキュメントや動画が充実しており、また、公式が状態管理などで推奨するライブラリや設計指針を提示してくれている。 |
TypeScript / Expo for web | 基本的なライブラリはかなり充実しており、デファクトスタンダードなものも検索すると見つけやすい。各ライブラリのメンテナンスはコミュニティに依存している部分が大きいため、よく考えて選定する必要がある。 | 基本的な実装についてはReactの公式ドキュメントやExpoの公式ドキュメントが充実している。ライブラリを含めた有効な設計指針については、 ネット上のReactの議論を参考にすれば大丈夫そう。 |
Kotlin / Compose for web | JVMのライブラリ自体はかなり多い。ただし、AndroidやCompose UI関連のライブラリはCompose for webでは利用できない場合が多い。 | ドキュメントはあまりないため、GitHubリポジトリを探すか、コミュニティのSlackチャンネルで情報を探す必要がある。 |
そして、Flutterを採用へ
上述した技術検証を元に、私たちはFlutterをPoCにおけるクライアントアプリ開発の技術スタックとして採用しました。
理由は以下の3点です。
- クライアントアプリ開発に不慣れなメンバーであっても、ドキュメントや参考情報が充実しており、本業のサーバーサイド開発の工数を圧迫しにくいと予想
- フレームワークの開発が活発でありながらもメンテナンス体制が整っているため、バージョンアップやライブラリの導入が容易であること
- PoCという特性上、通信環境が安定した環境で実行されるアプリであるため、パフォーマンスの欠点はあまり問題にならないこと
また、後付けの理由になってしまいますが、Flutterだけでは解決できない課題に遭遇した際にDart上でJSを実行できることもとても心強かったです。
私たちのシステムではKeycloakを認証基盤に使用しており、Keycloakの公式からFlutter用のKeycloakのクライアント向けライブラリは提供されていないため、JS用ライブラリをDartで動作させて認証を行っています。
おわりに
この記事では、PoCで使用するクライアントアプリの開発でFlutterを採用した経緯について紹介させていただきました。
現在、私たちはサーバーサイド開発と並行してクライアントアプリの開発も行っています。 今後さらに技術的な知見を深められたらこのブログで情報をアップデートしていきたいと思います。
関連記事 | Related Posts
Flutter Webで単体テストしてますか?
A Kotlin Engineer’s Journey Building a Web Application with Flutter in Just One Month
バックエンドエンジニアたちが複数のFlutterアプリを並行開発していく中で見つけたベストプラクティス
Woven Payment Solution開発G紹介
Are You Unit Testing with Flutter Web?
The Best Practices Found by Backend Engineers While Developing Multiple Flutter Applications at Once
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は未来の生活を実験するためのテストコースとしての街です。