ZOZOマッチアプリのアーキテクチャと技術構成

ZOZOマッチアプリのアーキテクチャと技術構成

はじめに

こんにちは、ZOZOの堀江(@Horie1024)です。2025年6月、新規事業として「ZOZOマッチ」をリリースしました。ZOZOマッチは、ZOZOとして初めてFlutterを採用したモバイルアプリです。これまでiOS/Androidそれぞれでの開発体制をとってきた中でFlutterでのクロスプラットフォーム開発に舵を切るのは、ZOZOにとって新しい挑戦でした。

本記事は、社外登壇イベント「ナレッジナイト」で使用した資料1をベースに、一部内容を加筆・整理してまとめたものです。私たちがFlutterを選定した背景と、実際のアーキテクチャ・技術構成について紹介します。ZOZOにおける新しい挑戦の記録であると同時に、これからFlutterを導入しようとするチームの参考になれば幸いです。

目次

ZOZOマッチとは

「ZOZOマッチ」は、ファッション領域を中心にサービスを展開してきたZOZOが、新たに取り組むマッチングアプリです。コーデ写真をプロフィールに載せることで自分らしい雰囲気を伝えられるほか、洋服の好みやスタイルを軸に相性の合う相手とつながれるのが大きな特徴です。

https://zozomatch.jp/

新規事業として立ち上がったこのプロジェクトは、スピード感のある開発とiOS/Androidプラットフォームへの同時展開が求められました。そこで採用したのがFlutterです。

Flutter採用の背景

私自身は、ZOZOマッチの開発チーム立ち上げのタイミングで手を挙げてプロジェクトに参加し、技術選定から関わりました。モバイルアプリを新規で開発するにあたり、iOS/Androidネイティブでの開発かクロスプラットフォーム(Flutter、React Native、KMPなど)での開発かの選択肢がありました。

結果としてZOZOマッチはFlutterでの開発を選択しましたが、その選択には「ビジネス的背景」・「チーム的背景」・「技術的背景」の3つの背景があります。

ビジネス的背景

アプリを開発するにあたりビジネス的な要求は次の3つでした。

  • 開発期間の短縮
  • 高品質なiOS/Androidアプリの同時展開が必須
  • 社内へのクロスプラットフォーム開発ノウハウの蓄積

開発期間の短縮とiOS/Androidアプリ同時展開に加えて、将来的な別プロジェクトへの展開を見据えてクロスプラットフォーム開発ノウハウの蓄積と開発体制の構築を求められたのが特徴的だと思います。

チーム的背景

社内事情を鑑み、開発チームにアサインできるメンバーの条件は次のとおりでした。

  • Androidエンジニア3名
  • 全員が自走してアプリ開発可能
  • クロスプラットフォームでの開発経験なし(1名個人開発でFlutter利用経験あり)

後述の技術的背景で触れますが、AndroidエンジニアにとってFlutterの学習が容易という実感があり、1名がFlutterでの開発経験があることからFlutterの採用について検討を開始しました。

この段階でFlutterを採用する上での懸念は、「高品質なアプリを開発可能なチームを即時立ち上げ可能か」でした。その後、Flutterでのアプリ開発に定評のある外部パートナーの支援を得ることになり、懸念を払拭できる手応えがあったため、Flutterを採用する方針としました。

技術的背景

Flutterを採用した技術的な背景として、次の点があげられます。

  • 独自レンダリングによりUIの統一感を担保
  • Androidエンジニアにとっての習得の容易性2

ZOZOマッチのUI実装では、デザイン仕様を忠実に再現することを求められ、Flutterの独自レンダリングによって統一感のあるUIを実現できる点はメリットでした。また、開発チームがAndroidエンジニア中心というチーム事情から、Androidエンジニアにとって習得が容易という点もメリットです。

「FlutterがAndroidエンジニアにとって習得が容易」というのは、ZOZOマッチの開発に参加したAndroidエンジニア3名の意見です。具体的には次のような点です。

  • UI構築: Jetpack Composeの考え方に近い宣言的UI
  • Riverpod:依存解決(Hilt的)+状態管理(ViewModel+StateFlow/SharedFlow的)
  • 開発環境:Android Studio / VS Codeで違和感なく利用可能
  • 非同期処理:Coroutines ⇔ async/awaitで概念が近い
  • 言語:DartはKotlin/Java経験者にとって習得が容易

UI構築

近年のAndroidアプリ開発では、Jetpack Composeは標準的なUIフレームワークになりました。ZOZOにおけるAndroidアプリ開発でもJetpack Composeは利用されています。

techblog.zozo.com

FlutterもJetpack Composeと同様に宣言的UIのフレームワークです。ComposableWidgetはいずれも「UI = 状態(データ)の関数」として定義し、同じ入力には同じUIを返します。両者ともレイアウトツリーを辿って制約を渡し、子がサイズを返し親が配置する流れは同じです。

また、レイアウトの構築で頻繁に使用するRow/Columnの使い方もほぼ一致しています。加えて、FlutterのContainerPaddingAlignのようなレイアウトを指定するWidgetは、Jetpack ComposeではModifierでまとめて記述します。記述方法が異なるだけで考え方としては同じなので理解は容易です。

Jetpack Compose経験者向けにFlutterでUIをどう実装するかは「Flutter for Jetpack Compose developers」にまとまっています。ボタンやリスト、スクロールビューといった基本的なUI要素がJetpack ComposeとFlutterを比較する形で網羅されており、UI構築の学習に役立ちました。

docs.flutter.dev

加えて、Flutter公式では「Flutter for Android developers」という記事も公開しています。既存のAndroidの知識をベースにFlutterによるアプリ開発のスタート地点として活用することを意図されたドキュメントで、こちらも非常に参考になりました。

docs.flutter.dev

Riverpod

Flutterでアプリを開発する上で欠かせないのが状態管理です。その中でもRiverpodは、依存関係の解決と状態の共有を一元的に扱える強力な仕組みですが、初学者にとっては学習の壁になりがちです。ここでAndroid開発の経験があるエンジニアであれば、Dagger HiltによるDI(依存性の注入)3、ViewModel/Flowによる状態管理4といった既存の知識に対応づけて理解できます。そのため、Riverpodの習得は「全く新しい概念の習得」ではなく、既存の知識の延長線上として取り組むことが可能です。

開発環境

Flutterは複数のIDE、エディタをサポートしており、その中から選択可能です。

docs.flutter.dev

Android Studioでの開発とデバッグもサポートしており、Androidエンジニアは使い慣れた開発環境でFlutterでのアプリ開発をスタートできます。特にFlutterの学習を始めた当初、使い慣れた開発環境を利用できるのは学習のハードルを下げる一因になりました。エミュレータの作成と管理、デバッガの利用方法もAndroidアプリ開発の知識を活用できる点もメリットかと思います5

非同期処理

非同期処理はアプリを開発する上で避けては通れませんが、つまずきがちなトピックだと思います。Flutter(Dart)ではasync/awaitで非同期処理を記述しますが、これはKotlin Coroutinesによる非同期処理の記述に近いです。例えば、2つのAPIを呼び出す場合、Kotlin Coroutinesでは次のように書くことができます。

suspend fun loadUser(userId: UserId): User = api.getUser(userId)
suspend fun loadLikes(userId: UserId): List<Like> = api.getLikes(userId)

// 直列に取得
suspend fun loadAll(userId: UserId) = try {
    val user = loadUser(userId)
    val likes = loadLikes(userId)
    user to likes
} catch (e: Exception) {
    // エラーハンドリング
    null
}

// 並列に取得
suspend fun loadAllParallel(userId: UserId) = coroutineScope {
    val userDeferred = async { loadUser(userId) }
    val likesDeferred = async { loadLikes(userId) }
    userDeferred.await() to likesDeferred.await()
}

Flutter(Dart)で同様の処理を記述すると次のようになります。

Future<User> loadUser(UserId userId) => api.getUser(userId);
Future<List<Like>> loadLikes(UserId userId) => api.getLikes(userId);

// 直列に取得
Future<(User, List<Like>)?> loadAll(UserId userId) async {
  try {
    final user = await loadUser(userId);
    final likes = await loadLikes(userId);
    return (user, likes);
  } catch (e) {
    // エラーハンドリング
    return null;
  }
}

// 並列に取得
Future<(User, List<Like>)> loadAllParallel(UserId userId) async {
  final futureUser = loadUser(userId);
  final futureLikes = loadLikes(userId);
  final results = await Future.wait([futureUser, futureLikes]);
  return (results[0] as User, results[1] as List<Like>);
}

このようにKotlin CoroutinesとFlutter(Dart)では非同期処理を同様に記述でき、すぐに理解できました。一方、スレッドの切り替えやキャンセル処理については異なる点があり、こちらは新しく学ぶ必要があります。

言語

KotlinとDartは基本的な文法が似ており、AndroidエンジニアにとってDartの習得は比較的容易ですが、DartにはKotlinにない独自の表現や仕組みも存在します。そこで、まず言語仕様を一通り理解しておくことでFlutterの学習をよりスムーズに進めることができます。言語仕様の理解には、書籍や公式ドキュメントを読むことをお勧めします。また、DartPadで簡単に挙動を確認できるので言語仕様の理解に役立ちます。次の表では、言語機能や制御構文の観点からKotlinとDartを比較しています。

観点 Kotlin Dart
クラス定義 - プライマリコンストラクタにval/varでプロパティ定義、初期化を同時に記述可能
- 追加の処理はinitブロックで実行
- フィールドは宣言後、コンストラクタでthis.fieldまたはイニシャライザリスト (:)により初期化
- 複数のコンストラクタ(名前付きコンストラクタやfactoryコンストラクタ)が定義可能
プロパティ/getter・setter val/varにより自動でgetter/setterとバッキングフィールドが生成 バッキングフィールドの概念はなくget/set構文で明示実装
可視性(アクセス修飾) - public/protected/internal/private
- internalはモジュール単位で有効
- キーワードによる修飾なし
- 先頭に_を付けるとライブラリ内スコープに限定
不変・定数 - val(再代入不可)
- const val(コンパイル時定数、top-levelやobject内などで利用可)
- final(再代入不可)
- const(コンパイル時定数、constコンストラクタ可)
関数引数/オーバーロード - デフォルト引数+名前付き引数
- メソッドオーバーロード可
- 名前付き引数(required/オプショナル)、デフォルト値も可
- メソッドオーバーロード不可(名前付きコンストラクタや引数で代替)
シングルトン/静的メンバ - objectでシングルトン
- companion objectで擬似static
- トップレベル関数/変数やstaticフィールドを使用
- シングルトンはfactoryコンストラクタで返すのが定石
mixin interfaceのデフォルト実装+
委譲 by、拡張関数で近い表現
mixinwithで適用可能
enum - enum classにプロパティ/メソッド
- 定数ごとの実装も可
- Enhanced enumでプロパティ/メソッド
- implements/with 可(extends は不可)
sealed - sealed class/sealed interface
- whenと網羅性チェックが強力
- Dart 3でsealed class
- switch式で網羅性チェック
if ifは式 - if は文
- 式としてはcond ? "A" : "B" を使用
when/switch when は式
パターン/スマートキャスト/網羅性
switch は文または式(Dart 3)
パターンマッチ
網羅性(式として使用した場合)
null安全 String?, 安全呼び出し ?., エルビス ?: String?, 安全呼び出し?., ??, ??=, ?..(cascade), ...?(spread)
コレクション操作 val doubled = list.map { it * 2 } final doubled = list.map((e) => e * 2).toList();
ジェネリクス 共変/反変(out/in)を宣言部で記述 実行時にも型情報を保持。共変/反変は型体系的には限定的
並行処理 - Coroutines+Dispatcher
- 構造化並行性
- Flowによる非同期ストリーム
- Future/async-await
- 重い処理はIsolate、Stream/async*/yield
カスケード演算子 なし ..: 同じオブジェクトに対して一連の操作を実行
final p = Paint()..strokeWidth = 2..style = PaintingStyle.stroke;
spread 演算子 なし - ...listで展開
- コレクション内でif/forと併用可
遅延初期化 lateinit var(non-nullのvar限定) late/late finalで遅延可能(nullableでも可)
拡張メソッド fun String.lastChar() = this[lastIndex] extension StringX on String { String lastChar() => this[length - 1]; }
データ/値型 data classequals/hashCode/copyが自動生成) - records((a, b)
- package:freezed がデファクトな代替
パターンマッチ whenis/in switch/ifでpatternsを利用可能
コード生成 KAPT/KSPによるコード生成 build_runnerによるコード生成

開発体制とパフォーマンス

ZOZOマッチアプリは、ZOZOエンジニア3名(現在は4名)+ 外部パートナーで開発を進め、リリース時点で自動生成のコードを除いて1000ファイル10万行ほどの規模になりました。時間経過でZOZO側メンバーのFlutterへの習熟度が向上し、開発のパフォーマンスとしては良い数値が出ていたと思います。Findy Team+6のサイクルタイム分析では、コミットからプルリクがマージされるまで33.7時間で、社内でも開発のリードタイムが短いチームでした。QA期間中における不具合の検出数もコントロール可能な範囲に収まり、最終的にスケジュール通りにリリースできました。

ZOZOマッチアプリチームのサイクルタイム分析結果

アーキテクチャ設計

ZOZOマッチアプリでは、UI/Domain/Dataの3層構造をベースにしたマルチパッケージ構成を採用しています。UI層は機能単位でfeaturesパッケージを作成し、水平方向への分割と垂直方向への分割を組み合わせた構成としました。アーキテクチャ設計にあたり、Androidの「Guide to app architecture」とGoogleが公開しているリファレンス実装アプリ「Now in Android App」を参考にしています。

ディレクトリ構造と依存関係図は次のようになっており、coresパッケージはappsパッケージ、featuresパッケージ、およびcoresパッケージ自身から呼び出されます。また、featuresパッケージはappsパッケージからのみ呼び出されます。

.
├── apps
│   ├── app
│   └── catalog
│
└── packages
    ├── cores
    │   ├── core
    │   ├── designsystem
    │   ├── domain
    │   ├── infra
    │   └── ui
    │
    └── features
        └── xxx

ZOZOマッチアプリのパッケージ依存関係

パッケージ間の依存管理にはMelosPub workspacesの併用を採用することでマルチパッケージプロジェクトを効率よく管理できています。加えて、Melosのワークスペーススクリプトでbuild_runnerを使ったコード生成やLint、テストなどのタスクを一括実行できるため、全体の開発効率が向上しました。

設計したアーキテクチャは、標準アーキテクチャとしてドキュメント化しGitHub Repositoryで管理しています。ドキュメント化することによってチーム内で共通認識を得られ、開発をスムーズに進めることができました。

状態管理

アプリ開発において「状態管理」は避けては通れないテーマです。公式ドキュメントでは、状態を大きくEphemeral state(短命な状態)とApp state(アプリ全体の状態)の2種類に分類しています。

docs.flutter.dev

Ephemeral stateは、Widgetツリーの一部にのみ関係する一時的な状態を指します。一方で、アプリ全体や複数画面にまたがって共有されるデータはApp stateと呼ばれます。ZOZOマッチでは、Ephemeral stateの管理にはflutter_hooks、App stateの管理にはRiverpodを使用する方針としました。

種類 管理対象 共有範囲 管理方法
Ephemeral state 一時的・ローカルなUI状態 1つのWidgetツリー内
Widgetの寿命に一致し、副作用なしで完結
flutter_hooks ボタンの押下状態、テキスト入力値
App state アプリ全体・永続的なデータ・キャッシュ・セッション 画面を越えて共有 riverpod ログイン情報、ユーザー設定

UI層

前述の通り、UI層は機能単位でfeaturesパッケージを作成します。パッケージには画面を構成するScreen、画面の状態を表すUI State、および状態を保持するStateHolderなどが属します。ディレクトリ構成は次の通りです。

.
└── sample
    ├── analysis_options.yaml
    ├── assets
    ├── build.yaml
    ├── l10n.yaml
    ├── lib
    │   ├── l10n.dart
    │   ├── screen.dart
    │   └── src
    │       ├── component
    │       │   ├── shared_component.dart
    │       │   └── sample
    │       │       └── sample_component.dart
    │       ├── gen
    │       ├── hook
    │       ├── screen
    │       │   └── sample_screen.dart
    │       ├── state
    │       │   └── sample_state.dart
    │       ├── state_holder
    │       │   └── sample_notifier.dart
    │       └── util
    ├── pubspec.yaml
    └── README.md

画面は「Screen」単位で分けてscreenディレクトリに配置し、ファイル名はsample_screen.dartのようにsuffixとしてscreenを付与します。UIを実装していく中で一部をWidgetとして切り出したい場合、componentディレクトリに配置します。特定の画面(ここではsample_screen)でのみ使用する場合、component/sampleディレクトリに配置します。もし、複数のScreenでコンポーネントを共通化したい場合、componentディレクトリ直下に配置します。

画面の状態を表すクラス「UI State」はstateディレクトリに配置します。次のコードはユーザーのプロフィール状態を表すクラスで、Freezedを使いimmutableなクラスとして扱います7

@freezed
abstract class UserProfileInformationState with _$UserProfileInformationState {
  factory UserProfileInformationState({
    required UserProfile userProfile,
  }) = _UserProfileInformationState;

  UserProfileInformationState._();
}

画面の状態を保持するクラスを「StateHolder」と呼びます。StateHolderが複数のfeaturesパッケージから参照される場合、StateHolderをDomain層に配置します。次のコードはUserProfileInformationStateを保持するStateHolderの例です。

@riverpod
class UserProfileInformationNotifier extends _$UserProfileInformationNotifier {
  /// プロフィール画面の表示に使用するデータを [build()] で取得する
  @override
  Future<UserProfileInformationState> build({required UserId userId}) async {
    final repository = ref.watch(userRepositoryProvider);

    final userProfile = await repository.getUserById(userId);

    return UserProfileInformationState(userProfile: userProfile);
  }
  ・
  ・
  ・
}

StateHolderでは、状態を取得するだけの場合FutureProviderを使用します。取得に加えてユーザーやシステムとのインタラクションによって状態の更新が発生する場合にはAsyncNotifierProviderを利用する方針としました。状態の更新が発生する場合、次のように単方向データフローでイベントを処理し、UI Stateの更新とScreenを再描画します。

  1. 現在のデータを取得
  2. 現在のデータからUI Stateを作成
  3. Screenを描画
  4. 入力されるイベントを処理
  5. データの更新処理
  6. 更新後のデータを取得
  7. 新しいUI Stateを作成
  8. Screenを再描画

単方向データフローによる状態の更新と再描画の流れ

Domain層

Domain層は、Data層とのやりとりをRepositoryパターンで抽象化し、UI層が画面を構築するためのデータ(モデル)を提供します。ディレクトリ構成は次の通りです。

.
└── domain
    ├── analysis_options.yaml
    ├── build.yaml
    ├── lib
    │   ├── data
    │   │   └── sample_repository.dart
    │   ├── model
    │   └── service
    │       └── sample_service.dart
    ├── pubspec.yaml
    ├── README.md
    └── test

UI層のStateHolderは、Repository経由でデータを取得します。次のコードは、ユーザーのプロフィール情報を取得するUserRepositoryです。UserRepositoryはApiClientのインスタンスを保持し、プロフィール情報を取得するためにAPIリクエストを行います。UserRepositoryのインスタンスはRiverpodで管理し、StateHolderからはuserRepositoryProviderよりインスタンスを取得します。

@riverpod
UserRepository userRepository(Ref ref) => 
    UserRepository(apiClient: ref.watch(apiClientProvider));

class UserRepository {
  UserRepository({required ApiClient apiClient})
    : _apiClient = apiClient;

  final ApiClient _apiClient;

  Future<UserProfile> getUserById(UserId id) async {
    final usersProfileApi = _apiClient.getUsersProfileApi();
    final data = await usersProfileApi.getUserProfile(userId: id);
    return UserProfile.fromApiModel(id: id, data: data);
  }
}

ここで、UserProfileクラスはドメインモデルです。アプリ内で使用するデータをドメインモデルとして表現し、APIのレスポンスモデルと分離しています。このため、ドメインモデルにfromApiModelメソッドを実装し、APIのレスポンスモデルからドメインモデルへの変換処理を用意しています。

/// ユーザーID
extension type const UserId(int value) implements int {}

/// プロフィール
@freezed
abstract class UserProfile with _$UserProfile {
  const factory UserProfile({
    /// ユーザーID
    required UserId id,
    
    /// 名前
    required String nickname,

    ・
    ・
    ・
  }) = _UserProfile;

  const UserProfile._();

  factory UserProfile.fromApiModel({
    required UserId id,
    required ProfileResponse data,
  }) {
    return UserProfile(...);
  }
}

ドメインモデルのモデリングは、プロジェクトの初期にメンバーで集まって行いました。要求仕様書とデザインから必要な概念を洗い出し、データ間の関係を整理した結果がドメインモデルに反映されています。モデリングの場に企画段階から関わっていたマネージャに参加して頂くことでメンバーの仕様への理解が深まりました。

ドメインモデルのモデリング

Repositoryに加えて複雑化したロジックの集約・再利用するためにServiceを用意しました。Serviceが参照するのはRepositoryのみで、Service同士は依存しません。特に認証、課金等の処理をServiceに集約しています。

@riverpod
AuthenticationService authenticationService(Ref ref) => AuthenticationService(
  ref.watch(tokenRepositoryProvider),
);

/// 認証処理をまとめたService
class AuthenticationService {
  AuthenticationService(this._tokenRepository);

  final TokenRepository _tokenRepository;

  Future<void> authenticateAndSaveToken() async {...}

  Future<void> refreshAccessToken() async {...}

  Future<void> deleteToken() async {...}
}

Data層

Data層では、APIやローカルストレージへのCRUD操作を実装しています。APIリクエストに使用するAPIクライアントとレスポンスモデルは、別リポジトリで管理するOpenAPI Documentから自動生成しています。Documentに変更が生じると自動でPull Requestが作成され、APIの実装と仕様の一貫性を維持しています。

APIクライアント更新のPull Request例

自動生成はGitHub Actionsで実現しており、その流れは次の通りです。workflow_dispatchでOpenAPI Documentの変更を受け取りOpenAPI GeneratorでAPIクライアントを生成しています。

  1. OpenAPI Documentを更新
  2. workflow_dispatchでZOZOマッチアプリリポジトリのWorkflowが起動
  3. OpenAPI Documentを取得
  4. APIクライアントを生成
  5. 生成結果のコミットとPull Request作成

次のようにジェネレータにはdart-dioを指定しAPIクライアントを生成します。

# APIクライアントを生成
openapi-generator-cli generate \
    --input-spec ${参照するOpenAPI Documentのパス} \
    --generator-name "dart-dio" \
    --output ${生成したクライアントの出力先} \
    --config ${設定ファイルのパス} \
    --template-dir ${カスタマイズしたテンプレートのディレクトリパス}

技術スタック

主要ライブラリ

ZOZOマッチアプリで使用している主要なライブラリは次の通りです。

  • ナビゲーション: go_router
  • 状態管理: riverpod, flutter_hooks
  • API通信: dio, graphql_flutter
  • データストア: flutter_secure_storage, shared_preferences
  • 認証/課金: flutter_appauth, in_app_purchase
  • テスト/品質管理: mocktail, test, alchemist, custom_lint

ライブラリはスタンダードなものを選定していますが、課金処理はin_app_purchaseを採用しつつ、バックエンドでレシート検証と重複購入の冪等処理、払い戻し/キャンセルの同期を自前実装しています。テストは、テストピラミッドに沿ってユニットテストを中心に用意しており、主要な機能に対してはalchemistによるゴールデンテストを作成しています。これらのテストやLintは、Pull Requestごと・特定のブランチマージ時にGitHub Actions上で実行しています。そして、graphql_flutterはメッセージ周りでGraphQLサブスクリプションを利用する目的で使用しています。

メッセージ機能(https://zozomatch.jp/より引用)

GraphQLを用いたメッセージ画面の詳細は、次の記事をご覧ください。

techblog.zozo.com

デザインシステム

ZOZOマッチではデザインシステムを用意しており、ColorやTypographyといったデザイントークンは、Dartコードに自動変換する仕組みを構築しています。また、コンポーネントは汎用コンポーネントとして実装し、Widgetbookでカタログ表示できる状態にしています。

デザインシステムの例: Buttonコンポーネント

デザインシステムやDartコードに自動変換する仕組み、Widgetbookでカタログ表示の詳細は、次の記事をご覧ください。

techblog.zozo.com

開発環境とAI活用

ZOZOマッチアプリの開発環境は次の通りです。

  • 言語/フレームワーク: Flutter(3.32.8)/ Dart(3.8.1)8
  • ドキュメント/チケット管理: Confluence / JIRA
  • デザインツール: Figma
  • 開発効率化: Melos, アプリ内デバッグメニュー9
  • CI/CD: GitHub Actions, Xcode Cloud
  • アプリ配布: TestFlight, DeployGate
  • エディタ/IDE: Cursor
  • AIツール: GitHub Copilot, Codex, Claude Code, Gemini CLIなど

エディタやAIツールについては、利用したいツールを選択できる環境が整っています。ZOZOマッチアプリチームでは、エディタをCursorに統一しています。加えて、複数の開発AIツールを調査や設計、ドキュメント作成、コーディング、コードレビューに活用しています。現時点では、どれか1つのツールに絞るというより複数のツールを併用して試行錯誤しています。また、自律型エージェントとしてはGitHub Copilot coding agentが利用可能です10

ZOZOにおける社内AI基盤の整備、生成AIの業務への活用とサービスへの応用については弊社CTO瀬尾の資料をご覧ください。

speakerdeck.com

まとめ

ZOZOマッチの開発では、Flutterを採用し、UI/Domain/Dataの3層をベースにしたマルチパッケージ構成を軸にアーキテクチャの設計と技術選定をしました。結果として短期間でiOS/Android両アプリを同時にリリースでき、品質と開発速度の両立を実現しています。

現在は、リリース後のフェーズとしてサービスの成長に注力しています。多くの課題がありますが、仮説検証を重ねながらより良い価値提供を目指して開発を続けています。その中で、開発プロセス全体へのAI活用も視野に入れ、より効率的で高品質な開発に挑戦していきたいです。

ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。ご興味をお持ちの方は以下のリンクからぜひご応募ください。

hrmos.co

corp.zozo.com


  1. https://speakerdeck.com/zozotech/zozo-match-architecture-technology-stack
  2. 2025年9月現在、Androidエンジニアにとっての習得の容易性という観点では、KMP + Compose Multiplatformという選択肢もあるかもしれません。
  3. DIとは何かを知るには、Android Developersの「Androidでの依存関係インジェクション」が参考になります。
  4. 状態管理の実装についてはGoogleが公開している「Now in Android App」や「Androidify on Android」といったアプリのコードが参考になります。
  5. 現在はCursorに統一しています。
  6. ZOZOはFindy Team+ Award 2025でOrganization Awardを受賞しました。
  7. UI Stateの作成は必須でなく、後述するドメインオブジェクトをそのまま状態管理に使うことを許容します。UI Stateがあると都合が良い場合はドメインオブジェクトを変換して利用します。
  8. 執筆時点(2025年9月)
  9. 開発・テスト時のみ使用できるアプリ内デバッグメニューを実装しています。特定の画面への遷移や時刻の変更、API通信先の変更など開発を効率化する機能を実装しています。デバッグメニューの詳細については、デバッグメニューでFlutterのアプリ開発をスムーズに! - ZOZO TECH BLOGをご覧ください。
  10. Devinは近日中に利用可能となる予定です。
カテゴリー