デバッグメニューでFlutterのアプリ開発をスムーズに!

デバッグメニューでFlutterのアプリ開発をスムーズに!

はじめに

こんにちは、新規事業部フロントエンドブロックの大野純平です。2025年度に新卒入社し、現在のチームに配属されました。2025年6月に新規事業としてリリースされた、全身見える直感型マッチングアプリ「ZOZOマッチ」のアプリ開発を担当しています。「ZOZOマッチ」は、ZOZOとして初めてFlutterを採用したモバイルアプリです。

zozomatch.jp

ZOZOマッチの開発では、様々な状態を再現する作業に多くの時間を費やしており、効率化が課題となっていました。

本記事では、この課題を解決するデバッグメニューの作り方と効果的な機能を紹介します。

目次

背景・課題

ZOZOマッチの開発業務の中で、コードレビューやバグ修正時に特定の状態を再現する作業が頻繁に発生しています。マッチングアプリという性質上、審査ステータス・写真の登録状態・時刻に依存するイベント・チュートリアルの表示状態など、非常に多くの状態が複雑に絡み合っています。これらの状態を手動で再現する作業は毎回10分以上かかり、エンジニアの集中力を削ぐだけでなく、状態の再現ミスによるバグの見逃しリスクも高まっていました。

アプリの画面数が100を超える規模となっており、特定の画面にたどり着くまでに複数の画面を経由します。さらに、期間限定キャンペーンや特定時刻の機能など、時間に依存したイベントのテストでは、その時刻になるまで待機するかシステム時刻を変更していました。

これらの非効率な作業により、本来の開発業務に集中できる時間が削られ、チーム全体の生産性低下を招いていました。

解決策:デバッグメニューの導入

これらの課題を解決するため、アプリ内にデバッグメニューを実装しました。

デバッグメニューは、開発・テスト時のみ使用できる特別な画面です。本番環境では動作しないため、セキュリティリスクを排除しています。この画面から、通常のユーザー操作では再現困難な様々な状態をUIから手軽に設定・再現できます。今回はそのデバッグメニューの中から効果的であった機能を紹介します。

デバッグメニューの呼び出し方法

デバッグメニューへのアクセス方法として、以下の2つの方法を実装しています。「端末を振る」については、shake_gestureパッケージを使用し、実現しました。

// 1. 端末を振る(ShakeGesture)
ShakeGesture.registerCallback(
  onShake: () {
    // 遷移する動作
  },
);

// 2. 同じタブを複数回連続タップ(5回の例)
if (tapCount.value == 5) {
  // 遷移する動作
}

デバッグメニュー画面

実装した機能と具体的な活用場面

1. ログ表示機能

アプリ内のイベントやエラーを時系列で確認できる機能です。開発中の問題を素早く特定するために、Talkerを採用しました。

Talkerを選んだ理由は次のとおりです。

  • 専用のログ画面があり、アプリ内で確認できる
  • ログレベル(debug、info、warning、error)を色分け表示でき、視認性が高い
  • 例外を自動で捕捉して記録できる
  • Dio、Riverpod、BLoCなどの主要パッケージと統合できる
  • フィルタリングで目的のログをすぐに絞り込める

Talker専用画面については、パッケージのREADMEに掲載されているWeb Demoをご覧ください。

デバッグメニューでは、次のログを表示します。

  • アプリケーション動作
    • 各画面のbuild実行とレンダリング状況
    • 画面遷移時のルート変更とスタックの変化
  • 外部サービス連携
    • Google Analyticsのユーザー行動イベント(表示・いいね・画面表示)
    • 送信の成功/失敗と送信内容の事前検証
    • FirebaseのInstallation ID設定・更新
    • 各エンドポイントの接続状態と設定値
    • 認証/トークン処理の進行状況
  • HTTP 通信
    • リクエスト(ボディ・ヘッダー・パラメータ)
    • レスポンス(ボディ・ステータスコード・処理時間)
  • 設定・環境情報
    • アプリ名、バージョン、ビルド番号、パッケージ情報
    • 環境(開発/本番)の切り替え状況
    • デバッグリポジトリの使用状況

ログ画面はリアルタイムで更新され、新しいログが上部に追加されます。ログレベルやキーワードで絞り込みでき、必要な情報にすばやく到達できます。テキストとして共有する機能もあり、チーム内の情報共有が容易です。非エンジニアでも簡単にログを共有できる点は大きなメリットとなりました。

Talkerは特に他チームとの連携に役立ちました。具体的には、QAチームやデザイナーチームから不具合報告を受けた際、再現時のログをチケットに添付してもらう運用にしました。これにより、ログを確認するだけで「バックエンドのレスポンスが誤っているのか」「アプリ実装が間違っているのか」を即座に判別でき、調査の初動時間を大幅に短縮できました。こうした迅速な対応を実現できたのは、非エンジニアでも簡単にログを共有できる仕組みがTalkerに備わっていたからです。

アプリチーム単体でも多くの利点がありました。Crashlyticsとの連携設定が容易で、Google Analytics導入時にはイベント送信の正しさを即座に確認でき、デバッグメニューでは難しい検証を補完できました。さらに、常に画面のルーティング情報が出力されるため、階層の深い画面にいても自分の位置を即座に把握できる点も便利でした。

この機能により、デバッグ時間の短縮、予期しないエラーの早期発見、API通信の可視化、状態管理の理解促進、分析イベントの検証を実現し、開発効率が大きく向上しました。

2. 画面遷移デバッグ

アプリ内の任意の画面へワンタップで直接遷移できる機能です。検索と履歴を備え、開発中の移動コストを下げます。ZOZOマッチの画面数は100を超えるため、通常操作では深い階層(例:オンボーディング途中の全身写真を登録する画面)への到達が手間でした。

この機能はZOZOマッチの遷移で採用しているgo_routerの仕組みを応用することで実現しています。この記事では詳細を割愛しますが、RouteConfigurationから定義済みのパス情報を抽出する仕組みにすることで、新規画面の追加時も追加実装が不要な作りにしています。

オンボーディング途中の全身写真を登録する画面

画面遷移デバッグでは様々な機能が搭載されています。実際の画面をもとに、機能詳細を解説していきます。

画面遷移デバッグ画面

  1. 画面IDをテキスト入力して遷移したい画面を検索できる機能。ZOZOマッチには100種類以上の画面があるため、この検索機能で目的の画面へすぐアクセスが可能。
  2. 直近でデバッグメニューから遷移した画面の履歴を表示する機能。特定の画面へ繰り返し遷移する際に履歴から開けるので、毎回検索する手間を省ける。
  3. 全てのルートをリスト形式で表示する。この一覧をタップすると該当の画面へ1ステップで移動ができる。

このツールにより、深い階層の画面へ数秒で移動でき、開発時間を短縮できました。全画面を容易に確認できるため、リグレッションの早期発見にも有効です。

現在の課題は、ユーザー固有の画面(例:相手のプロフィール)へ遷移する際に、ユーザーIDなどのパラメータを指定できず、動的ルートに対応できていない点です。今後は、パラメータ入力に対応した遷移デバッグの実装を検討しています。

3. SharedPreferencesデバッグ

SharedPreferencesに保存されているデータの確認・編集機能です。アプリ内の各種フラグや設定値を管理するSharedPreferencesの値を、開発中に簡単に確認・変更できるデバッグ画面として実装しています。

ZOZOマッチでは、チュートリアル表示フラグやIn-App Reviewのリクエスト制御など、通常は特定の条件でしか変更されない値が多数存在します。これらの値を手動で操作できることで、開発・テスト効率が向上しました。

SharedPreferencesデバッグ画面

例えば、チュートリアルの再確認では、一度表示した後は通常再表示されないためデザイン変更や修正後の確認が難しい状況でしたが、フラグを任意のタイミングでリセットすることにより再表示が可能になりました。また、In-App Reviewのテストでは、同一ユーザーには一定期間レビューを表示しないフラグを制御できるため、表示履歴を削除し、開発中に何度でも表示をテストできるようになりました。

この機能により、検証のリードタイムを短縮し、開発サイクルを高速化できます。

4. API通信モック機能の切り替え

バックエンド開発と並行してフロントエンドを進める際、未実装APIや再現しづらいレスポンスが課題でした。本番用Repositoryとダミーデータを返すDebugRepositoryを切り替え、API通信をモック化できるようにしました。切り替えはデバッグメニューから行えます。

API通信モック機能の切り替画面

実装アプローチは以下のとおりです。

DebugRepositoryは本番Repositoryを継承し、必要なメソッドのみをオーバーライドしています。

// 本番用Repository
class UserRepository {
  Future<User> getUserById(String id) async {
    final response = await apiClient.get('/users/$id');
    return User.fromJson(response.data);
  }
}

// Debug用Repository
class DebugUserRepository extends UserRepository {
  @override
  Future<User> getUserById(String id) async {
    // ダミーデータを返す
    return User(
      id: id,
      name: 'テストユーザー',
      age: 25,
      );
    }
  }

Riverpodによる動的切り替えでは、デバッグフラグに応じてRepositoryを実行時に切り替えます。

// Providerでの使用例
@riverpod
UserRepository userRepository(Ref ref) => selectRepositoryByBuildMode(
  // 本番用のRepository
  () => UserRepository(
    apiClient: ref.watch(apiClientProvider),
  ),
  // デバッグ用の設定
  debugConfig: (
    factory: () => DebugUserRepository(),
    enabled: ref.watch(isDebugModeProvider),
  ),
);

全てのRepositoryで統一的に切り替えられるよう、selectRepositoryByBuildMode という共通ヘルパーを用意しています。

T selectRepositoryByBuildMode<T>(
  T Function() releaseOrProfile, {
  ({T Function() factory, bool enabled})? debugConfig,
}) {
  // リリースビルドでは常に実APIを使用
  if (!kDebugMode) {
    return releaseOrProfile();
  }

  // デバッグビルドでフラグがONの場合はDebugRepositoryを使用
  if (debugConfig != null && debugConfig.enabled) {
    final debugInstance = debugConfig.factory();
    return debugInstance;
  }

  // それ以外は実APIを使用
  return releaseOrProfile();
}

この仕組みにより、次が可能になります。

  • ビルドモード連動 - kDebugModeでリリースビルドを判定し、本番では常に実APIを使用
  • 動的切り替え - デバッグビルドでは画面上のトグルで即座に切り替え
  • 可視化 - 現在使用中のRepositoryをコンソールで確認

また、デバッグメニューに統合したことで、画面上のトグルで手早くモック機能を切り替え、本番APIとモックデータを用途に応じて使い分けられます。

API通信モックを導入したことで、APIの実装完了を待たずにUIを実装・確認でき、バックエンドとフロントエンドの並行開発が可能になりました。また、空リスト・大量データ・特殊文字列などのデータパターンを簡単に再現でき、動作確認が容易になりました。

バグ修正時は、DebugRepositoryでレスポンスを任意に制御し、エラー状態を即時に再現・検証できます。

5. 時刻デバッグ機能

ZOZOマッチでは特定時刻の機能が多く実装されており、その動作確認が開発の大きな課題となっていました。チャンスタイム1やレコメンド更新2など、特定の時刻でのみ動作する機能をテストするために、従来は端末の時刻設定を変更する必要がありました。しかし、これによりバックエンドから取得できるデータとの整合性に問題が発生しました。

この問題を解決するため、サーバー時刻ヘッダー機能を導入しました。すべてのAPIレスポンスに含まれるdateヘッダーからサーバー時刻を取得し、端末時刻との差分(オフセット)を計算します。これにより、端末の時刻設定に依存しない正確な時刻を取得できるようになりました。

// 従来の実装(端末時刻に依存)
final now = DateTime.now();  // 端末の時刻設定に影響される

// 新しい実装(サーバー時刻を使用)
final now = serverTimeClock.now(ref);  // サーバー時刻を基準とした正確な時刻

デバッグビルドでは、特別なヘッダーx-znm-debug-timerに任意の時刻を指定できます。サーバー側はそれを受け取り、dateヘッダーとは別でx-znm-debug-timer-responseに特殊な時刻を設定してくれる仕組みになっています。この仕組みを利用し、アプリ内だけで有効な仮想時刻を使い、特定時刻の機能を検証します。任意の時刻はデバッグメニューから容易に切り替えでき、テスト効率が向上します。

時刻デバッグ機能画面

例えば、「20:20」など特定の時刻にのみ動作する機能のテストに活用することにより、任意のタイミングで動作確認が可能になりました。これにより、端末時刻の変更に伴う副作用を排除し、特定時刻の機能を安全かつ効率的に検証できるようになりました。

まとめ

本記事では、Flutterのデバッグメニュー構築を紹介しました。デバッグメニューは、単なる効率化を超えて、チームの働き方と開発体験を変える基盤になり得ます。今後も実運用のフィードバックを取り込み、より多くの検証・開発フローで最適化を進めていきます。

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

hrmos.co

corp.zozo.com


  1. 20時20分になるとその日の新着ユーザーを無料で限定人数いいねできる機能のことです。
  2. 特定時刻になると、ホームタブに表示されるユーザーが更新されます。
カテゴリー