
Android
イベント
マガジン
技術ブログ
こんにちは、ソリューションアーキテクトの田邊です。2026 年 6 月 25 日から 26 日にかけて幕張メッセで開催された AWS Summit Japan 2026 の AWS for Industries Zone(ブース番号 044 )にて、スマートグラスと生成 AI エージェントを組み合わせた倉庫ピッキング支援デモを展示しました。本ブログでは、展示内容とその裏で動いている AWS サービス構成を、デモの体験に沿ってご紹介します。 背景:倉庫ピッキング業務の課題 倉庫のピッキング業務は物流オペレーションの中でも人手に依存する割合が高く、ハンディターミナル(専用の携帯端末)の導入は進んでいるものの、以下のような課題が現場に残っています。 両手が塞がる : 商品を持つ・置く動作と端末操作が競合し、重量物のハンドリング時に効率が落ちる 視線の移動 : 端末画面と棚を交互に見る必要があり、ピッキングミスや作業テンポの低下につながる 教育コスト : 新人・パート作業者が端末操作や棚配置を覚えるまで時間がかかる 多言語対応 : 外国人作業者が増える中、日本語のみの指示系統ではオンボーディングに時間がかかる これらの課題に対して、スマートグラスと生成 AI エージェントを組み合わせ、「見るだけ・話すだけ」で業務システムにアクセスできる新しい業務体験を提案するデモを企画しました。 デモの全体像 今回のデモでは、倉庫のピッキング業務を題材に、スマートグラスと生成 AI エージェントによるハンズフリー業務支援を紹介しました。特徴は以下の2点です。 1. 音声だけで完結する業務システム操作 「ピッキングリストをください」と話しかけるだけで、倉庫管理システム( WMS )から作業リストを取得し、音声で案内 商品の QR コードをスマートグラスでスキャンするだけで、ピッキング完了が自動で記録 端末画面のタップは不要。両手で商品を扱いながら業務を進められる 2. 自然な多言語対話 Amazon Nova 2 Sonic により、人と話しているような低遅延で自然な音声対話を実現 英語・スペイン語・ドイツ語・フランス語・イタリア語・ポルトガル語・ヒンディー語に対応(話した言語を自動判定して同じ言語で応答) 商品の取扱注意など業務ナレッジも、社内マニュアルから探して読み上げ 使用したスマートグラス 本デモでは、Android を搭載したスマートグラス 2 機種を用意し、来場者に体験いただきました。両機種とも AI エージェントと対話するアプリを単体で動作させることができ、それぞれ形状・視認性・装着感に特徴があります。 Vuzix M400 — Android11 を搭載し単体で動作するモノキュラー(単眼)型のスマートグラスです。1,280 万画素のカメラでバーコード読み取りに対応し、本体 68 g、IP 67の防水防塵性能で倉庫環境にも適しています。ノイズキャンセリングマイクを搭載しており、周囲に環境音がある現場でも音声を正確に拾うことができます。 RayNeo X3 Pro — Android を搭載したフルカラー MicroLED ディスプレイを持つ眼鏡型のスマートグラスです。バーコード読み取りに加え、音声・映像を組み合わせたマルチモーダルな対話に対応します。輝度 6,000 ニットの高い視認性で、明るい場所でも表示内容がしっかり見えます。装着感が眼鏡に近く、より日常的なユースケースにも馴染むデザインです。 なお、本デモのアプリは Android で動作する仕組みのため、Vuzix・RayNeo に限らず、Android 対応の他のスマートグラスやタブレット・スマートフォンでも同様の体験を実現できます。 アーキテクチャ構成 デモのアーキテクチャは以下のような構成になっています。 スマートグラス上の Android アプリはシンプルな役割に絞り、会話の理解・業務データの取得・回答生成といったロジックはすべてクラウド側の Amazon Bedrock AgentCore Runtime に集約しています。全体の処理の流れは次のとおりです。 認証 : 端末にクラウドの長期パスワード的な情報を置かず、Amazon Cognito と AWS Lambda により一時的な接続 URL を発行するセキュアな仕組み 音声送受信 : スマートグラスから作業者の音声をリアルタイムに Amazon Bedrock AgentCore Runtime へ送信。Amazon Nova 2 Sonic が音声を直接理解し、応答の音声を返す 業務データへの問い合わせ : エージェントが必要に応じて Amazon Bedrock AgentCore Gateway を介し、ツール(関数)を呼び出し、Amazon DynamoDB ( WMS データ)や Amazon Bedrock Knowledge Bases (社内マニュアル)からデータを取得 この構成により、スマートグラス側は音声とカメラの入出力に専念でき、エージェントの機能追加やプロンプト調整はクラウド側の変更だけで反映されます。以下、デモのシナリオを順に追いながら、各ステップで動いているサービスを見ていきます。 ① 音声でタスク取得 作業者がスマートグラスに「ピッキングリストをください」と話しかけると、生成 AI エージェントが WMS から作業リストを取得し、棚番号・商品名・数量を音声と画面で案内します。 裏側では、スマートグラスから送られてきた音声をクラウド上の Amazon Nova 2 Sonic が受け取ります。Amazon Nova 2 Sonic は音声を直接理解し、そのまま音声で応答を返せる生成 AI モデルです。従来の「音声を一度文字に起こしてから生成 AI に渡し、生成された文章を再び音声にする」という 3 段階の処理と違い、音声のまま処理するため、人と話しているような自然で低遅延なやり取りが可能になります。作業者の意図が「ピッキングリストの取得」だと判断されると、エージェントが WMS 用のツールを呼び出し、Amazon Dynamo DB からリストを取り出して音声で読み上げ、同時にタスク一覧を画面に表示します。 ② QR スキャンで商品照合・完了 案内された棚に移動し、商品のQR コードをスキャンすると、エージェントが商品を照合し、ピッキング完了を WMS に自動で記録します。次のアイテムがある場合は続けて音声で棚の場所を案内し、全アイテム完了時には作業時間もフィードバックします。 ここでは、音声とカメラという異なる入力を、同じ会話の流れの中で扱っています。QR スキャンの結果はテキスト情報として Amazon Nova 2 Sonic に渡り、エージェントは「今、この作業者は音声で受けたタスクの商品をスキャンした」と文脈を理解した上で、Amazon Dynamo DB に完了記録を書き込みます。作業者は「モードを切り替える」といった意識をせず、話す・スキャン・話す、というテンポで作業を進められます。 ③ 視覚支援と業務ナレッジ照会 「棚の場所が分からない」と聞けば倉庫マップで棚位置をハイライト表示、「タイミングベルトの取扱注意点は?」と聞けば社内マニュアルから該当箇所を音声で読み上げます。 業務ナレッジの音声応答は、Amazon Bedrock Knowledge Bases という機能で実現しています。これは、事前に社内のピッキングマニュアルなどのドキュメントを Amazon S3 に置いておくと、生成 AI が質問の意図に合わせて関連する記述を意味的に検索してくれる仕組みです。エージェントは検索結果をもとに要点をまとめ、Amazon Nova 2 Sonic を通じて自然な音声で回答します。マニュアルを更新しても、それを取り込み直すだけで、エージェントの回答が常に最新の内容に保たれます。 1 会話あたりのコスト ブース来場者から多くいただいた質問がコスト面でした。本デモの構成で、ピッキング 4 アイテムを約 3 分で処理する会話1回あたりのコストは、およそ 8.9 円( 1 USD = 160 円換算)です。1 端末あたり 100 会話/日 × 30 日で月額約 26,720 円と、実運用に耐える水準です。コストの大半(約 78 %)は音声の生成( AI の応答音声)が占めるため、AI の応答を簡潔にする工夫がコスト削減に最も効きます。 期待される効果 本ソリューションの導入で、以下のような効果が期待できます。 作業者の身体的・認知的負荷の低減 : 端末操作から解放され、視線を作業対象に集中できる 教育コストの削減 : 音声で作業指示・マニュアル照会ができ、新人・短期雇用者の即戦力化が容易 多言語対応による人材活用 : 外国人作業者にも母語で指示・案内が可能 既存 WMS を活かした導入 : 現行の業務システムを置き換えず、生成 AI エージェントから連携 本デモのコアである「生成 AI エージェント × ウェアラブルデバイス × 音声インタラクション」の組み合わせは、倉庫ピッキングに限らず、「両手を使う現場作業×システム連携」が求められる幅広い領域に応用可能です。製造ラインでの作業指示と部品照合、医療現場での検体照合と電子カルテ入力、設備点検・警備での手順書参照とレポート入力、店舗のバックヤード在庫確認など、応用先は多岐にわたります。 まとめ AWS Summit Japan 2026 で展示した「スマートグラス × 生成 AI エージェント」は、倉庫ピッキング業務における「両手が塞がる」「教育コストが高い」「多言語対応が難しい」という課題に対し、見るだけ・話すだけで業務システムにアクセスできる新しい業務体験を提案するものです。Amazon Bedrock AgentCore Runtime と Amazon Nova2 Sonic の組み合わせで、サーバレスで実装できます。物流業界は 2024 年問題をはじめ様々な課題に直面していますが、生成 AI エージェントを既存の業務システムに繋ぐことで、現場の負荷を減らしながら生産性を高めることが可能です。ご興味のある方は、担当ソリューションアーキテクトまでお気軽にご相談ください。 会場では倉庫ピッキング以外のユースケースについても多くのご相談やディスカッションをいただき、このソリューションパターンへの関心の高さを実感しています。 この展示は、ソリューションアーキテクト横山、駒野、山本、田邊が担当しました。
株式会社エブリーは、「前向きなきっかけを、ひとりひとりの日常にとどける。」というミッションのもと、デリッシュキッチン、retail HUB、トモニテ、MOMENTH といった BtoC・BtoB を横断するメディアやサービスを展開しています。同社では、小売事業者向けサービス retail HUB におけるピッキングサービスの新規開発にあたり、データベースとして Amazon Aurora DSQL を採用しました。本ブログでは、お客様の開発チームに伺った Aurora DSQL 採用の背景、導入の取り組み、そして導入後に得られた効果についてご紹介します。 対象システム 今回 Aurora DSQL を採用したのは、 retail HUB 事業のネットスーパーサービスにおけるピッキングサービスです。 retail HUB は、お客様が提供する小売事業者向けの DX ソリューションです。店頭サイネージによるレシピ提案、デジタルチラシ、ネットスーパーアプリの提供、店頭運営の効率化、CRM によるロイヤルカスタマー育成まで、小売事業者の販促プロセス全体をデジタル化し、業務効率化ときめ細やかな顧客アプローチを実現しています。 retail HUB 事業のネットスーパーサービスでは、店舗側が売り場から注文商品をピックアップする作業が発生しますが、従来この運用は紙のリストで行われており、商品を探すのに時間がかかることや習熟度による作業スピードのばらつきなどにより人的ミスの防止が難しく、誤ピックによる取り直しが発生するといった課題がありました。この問題を解決するため、ピッキングアプリ「retail HUB Picker」の開発が求められました。 アーキテクチャ ピッキングサービスのアーキテクチャは、ピッキングアプリ(Android アプリ)から Amazon ECS + ALB で構成された API サーバーに接続し、担当者の認証管理には Amazon Cognito を利用しています。バックエンドのデータベースとして Aurora DSQL を採用し、ピッキング対象の商品情報やピッキング状態、担当者情報の格納・参照に使用しています。 ピッキングサービスのシステムアーキテクチャは以下のとおりです。今回新規に開発したピッキングシステムは既存のネットスーパーシステムとは分離しており、注文データを連携する箇所のみで結合しています。 データベース選定の背景 今回のピッキングサービスは新規開発であり、データベースの選定にあたってはシステムの特性に合わせた検討が行われました。データベースに求められる要件は以下でした。 安定性と可用性 ピッキング作業中にシステムが停止すると店舗のピッキング業務に影響を与え、導入店舗数が増えるほどその影響範囲も大きくなるため、安定性と高い可用性が重要な要件でした。また、小売店舗は土日も稼働しているため、運営側の都合でメンテナンス時間を設定しにくいというビジネス上の課題も考慮する必要がありました。 低コストによるスモールスタート 初期フェーズではリクエスト数が限定的であり、構築・運用にあまりコストをかけたくないという要望がありました。一方で、今後サービスの成長に伴いトラフィックの増加も想定されるため、高いスケーラビリティを備えたデータベースであることが望ましいと考えていました。 運用負荷の軽減 メンテナンスウィンドウに伴う運用負荷が課題となっていました。新規サービスでは、こうした運用負荷を極力下げたいという考えがありました。 Aurora DSQL を選択した理由 お客様では当初、既存システムで利用している Amazon RDS for MySQL や Amazon Aurora MySQL の導入を想定していました。しかし、上記の要件を踏まえて検討を進めた結果、サーバーレスの分散データベースである Aurora DSQL が採用されました。その理由は以下の通りです。 安定性と可用性 — シングルリージョン構成で 99.99%(マルチリージョン構成では 99.999%)の可用性を備えたサーバーレスの分散データベースであり、インフラ管理が不要。ピッキング作業中のシステム停止リスクを低く抑えられる 低コストによるスモールスタート — 従量制の課金モデルで初期コストを抑えつつ、将来のサービス成長に伴うトラフィック増加にも自動スケールで対応できる 運用負荷の軽減 — パッチ適用がダウンタイムなしで自動的に行われるため、関係者との調整や作業工数が不要になる 加えて、Aurora DSQL が PostgreSQL との互換性を備えていたことで、社内の既存知見を活かしながら導入できる点も後押しとなりました。また、お客様の開発チームには分散データベースへの技術的な関心が高く、Aurora DSQL を通じて得られる知見が今後の技術選定の指標になるという期待も大きく、採用の決め手の一つとなりました。 Aurora DSQL とは Aurora DSQL は、AWS が提供するサーバーレスの分散 SQL データベースです。従来のデータベースのようにクエリ処理・ストレージ・トランザクション管理が密結合した構成ではなく、それぞれが独立したコンポーネントに分かれています。各コンポーネントがワークロードに応じて個別にスケールするため、特定の処理がボトルネックになりにくく、低レイテンシーで強固な一貫性を実現しています。また、PostgreSQL との互換性を備えており、既存の PostgreSQL の知見やツールを活かして開発を進めることができます。 お客様が選定の理由に挙げた以下の 3 つについて、Aurora DSQL の特性を簡単に説明します。 安定性と可用性 Aurora DSQL では、すべての書き込みトランザクションを分散トランザクションログにコミットし、コミットされたすべてのログデータを 3 つのアベイラビリティーゾーンにあるストレージレプリカに同期レプリケーションします。これにより、Aurora の Multi-AZ 構成と同等の 99.99% の可用性を実現しています。以下の図は、Aurora DSQL の可用性に関する特性を示しています。 低コストによるスモールスタート 課金はリクエスト数やデータ量に応じた従量制で、リクエスト数が少ない段階ではコストを低く抑えられます。アーキテクチャを変更することなく自動的にスケールするため、小規模な構成から始めて、トラフィックの増加に応じてシームレスにスケールすることができます。以下の図は、Aurora DSQL のコストモデルを示しています。 運用負荷の軽減 サーバーのプロビジョニングやパッチ適用、インフラのアップグレードといった管理作業が不要です。パッチ適用やセキュリティアップデートはダウンタイムなしで自動的に処理されるため、メンテナンスウィンドウの調整や計画停止が不要です。以下の図は、Aurora DSQL と従来のデータベースにおけるメンテナンスの違いを示しています。 開発時の課題 開発スケジュール 開発スケジュールは以下のとおりです。 時期 内容 2025年5月 小売事業者に対するヒアリング(事業部門+開発部門) 2025年6月 運用分析、開発要件定義、分析要件定義、システム設計 2025年7月〜8月 開発(アプリケーション、API サーバー、インフラ、分析基盤) 2025年9月上旬 社内 QA 2025年9月中旬 リリース、小売事業者にて検証・運用開始 Aurora DSQL を用いた開発にあたっては、PostgreSQL との互換性に関していくつかの課題がありました。 スキーマ変更の制約 Aurora DSQL ではカラム追加時にデフォルト値を指定する構文がサポートされていないことが開発中に判明しました。PostgreSQL では一般的に使用される構文であるため、既存の知見をそのまま適用できないケースでした。対処として、カラムのみを追加し、デフォルト値や NOT NULL 制約はソフトウェアレベルで保証する実装としました。なお、サービスリリース前であったため、最終的にはテーブルを再作成することで問題を回避しました。このように、事前のドキュメント確認だけでは把握しきれず、実際の操作を通じて発見される差分もありました。 ローカル開発環境との互換性 当初ローカル開発では PostgreSQL のコンテナを使用していましたが、 CREATE INDEX を CREATE INDEX ASYNC に変更する必要があるなど、Aurora DSQL との互換性がない部分が判明したため、ローカル環境でも Aurora DSQL を使用する方式に切り替えました。 トランザクションサイズの制限 Aurora DSQL にはトランザクションデータ件数に制限があるため、大量のデータを一括で処理する実装には工夫が必要でした。この制限を回避するため、大量のデータ投入処理においては全体を 1 トランザクションにまとめず、処理を分割する設計としました。具体的には、データ量が大きい投入処理ではトランザクションを使わずに実行し、途中でエラーが発生しても何度でもやり直せる設計としました。その上で、整合性の担保が必要な少量のデータ更新処理だけをトランザクション内で実行するようにしました。 こうした課題を一つずつ解消しながら開発を進め、ピッキングサービスは無事にローンチを迎えました。 ローンチと導入後の効果 2025 年 9 月中旬にローンチしてから数ヶ月が経過していますが、大きな問題は発生しておらず、安定した稼働を続けています。 導入後に得られた効果は以下のとおりです。 コスト削減 Aurora PostgreSQL(Provisioned)で想定していたインスタンスサイズと比較すると、コストは 90% 以上低くなっています。従量制のコストモデルにより、初期フェーズにおいて大幅なコスト削減を実現しました。 以下は、今回のワークロードを想定したコスト比較の試算です。Aurora PostgreSQL(Provisioned)を 100 とした場合の相対コストを示しています。 構成 Pattern S コスト比 Pattern M コスト比 Pattern L コスト比 Aurora PostgreSQL (Provisioned) 100% 100% 100% Aurora Serverless v2 41% 41% 66% Aurora DSQL 4% 6% 12% 安定稼働とメンテナンスフリー サービス導入後は安定して稼働しており、パフォーマンス上の問題も発生していません。メンテナンスウィンドウが不要であるため、基盤のメンテナンス作業そのものだけでなく、それに伴う小売事業者やサービス利用者との調整業務も不要となりました。既存システムでは 3 ヶ月ごとのパッチアップデートのたびに、メンテナンス時間帯の調整、影響範囲の洗い出し、小売事業者や関係部署への連携が必要でしたが、Aurora DSQL ではこうした業務コストがゼロになり、運用負荷が大きく削減されました。 スケーラビリティの確保 現時点でのリクエスト数の変動については特に大きな問題は発生していません。今後のサービス成長に伴うトラフィック増加についても、検証の結果スケーリングに問題がないことを確認しており、今後のトラフィック増加にも不安なく運用できる見通しです。 お客様の声 お客様に Aurora DSQL の導入効果について伺ったところ、次のように述べています。 スケーラビリティと安定性の要件が高いシステムにおいて Aurora DSQL を選択する利点は高いと考えています。 – 内原 章 氏 株式会社エブリー 開発本部 開発2部 部長 今後の展望 お客様では、今回のピッキングサービスでの導入実績を踏まえ、スケーラビリティの要件が高いシステムにおいて Aurora DSQL を選択肢として検討していく方針です。新規開発だけでなく、既存システムのリプレースにおいても Aurora DSQL の活用を視野に入れています。 また、Aurora DSQL のさらなる進化への期待として、PostgreSQL との互換性向上を挙げています。特に RLS(Row Level Security)のような機能がサポートされることで、より幅広いユースケースに対応しやすくなると考えています。 Aurora DSQL は、高い可用性とスケーラビリティを必要とするシステムにおいて、運用負荷とコストの低減が期待できるデータベースです。本ブログが、同様の課題を持つお客様にとって参考になれば幸いです。
はじめに こんにちは、新規事業部フロントエンドブロックの 大野純平 です。2025年度に新卒入社し、現在のチームに配属されました。チームでFlutter製モバイルアプリを開発する中で、新規プロジェクトの立ち上げを効率化するための社内テンプレートリポジトリの整備を進めています。 このテンプレートを育てる中で、 Flutter 3.44 へのアップデートに伴うCocoaPodsからSPMへの移行対応が積み残しになっていました。あわせて、このタイミングでdev / stg / prdの3環境対応も新規に追加しました(3 flavor化自体はSPM移行の必須要件ではなく、テンプレート整備の一環で同時に着手したものです)。本記事では、 CocoaPodsからSPMへの移行 と、 3 flavorを mise run setup で一括整備する仕組み を紹介します。あわせて、Flutterにおけるビルド構成ファイルを解説します。 目次 はじめに 目次 Flutterプロジェクトのビルド構成 iOS側のしくみ Android側のしくみ Dart側のしくみ 3層を貫く全体像 背景・課題 3環境の設定整合が壊れやすい 解決策の全体像 実装内容と具体的な解決策 1. CocoaPodsを撤去してSPMに移行する CocoaPodsの統合を解除する Crashlytics dSYMのパスを更新する 2. iOS:xcconfigでBuild Configurationを9パターンに整える Firebase plistをBuild Phaseで差し替える 3. Android:productFlavorsとgoogle-services.jsonをflavorごとに配置する 4. Dart:firebase_optionsをセレクタshim化する main.dartでduplicate-appを処理する 5. mise run setupで全flavorを一括生成する まとめ Flutterプロジェクトのビルド構成 Flutterアプリは iOSネイティブ層・Androidネイティブ層・Dart層 の3つが組み合わさっています。環境(flavor)の切り替えとは「3層すべてに、同じflavor用の設定を選ばせること」です。 iOS側のしくみ Xcodeは以下の単位を組み合わせてビルドを定義します。 単位 役割 Flutter デフォルト Project ( .xcodeproj ) プロジェクト全体の定義。実体は project.pbxproj Runner.xcodeproj Target ビルド成果物の単位 Runner Build Configuration ビルド設定値の集合 Debug / Profile / Release の 3 種 Scheme Run / Test / Profile / Analyze / Archive の各アクションで使う Target と Configuration を定義する起動設定 Runner 1 つ xcconfig はBuild Configurationの値をテキストで宣言するファイルです。別のxcconfigを #include で取り込んだとき、同じ変数があれば 後から読み込んだ値が優先 されます。共通値を書いたxcconfigを先に、flavor固有値を書いたxcconfigを後に重ねれば、最終的な値が組み上がります。 Info.plist の CFBundleIdentifier は $(PRODUCT_BUNDLE_IDENTIFIER) を参照しています。そのため、xcconfig側で PRODUCT_BUNDLE_IDENTIFIER を決めればBundle IDが自動的に解決されます。 iOS・macOSで外部ライブラリを使うときの依存管理ツールには、 CocoaPods と Swift Package Manager(SPM) の2つがあります。Flutterプロジェクトでは長らくCocoaPodsが標準でしたが、Flutter 3.44からiOS・macOSの両方でSPMがデフォルトに切り替わりました。両者の違いは以下のとおりです。 CocoaPods Swift Package Manager (SPM) 提供元 OSS(Ruby 製) Apple 公式 設定ファイル Podfile Package.swift (Xcode では Package Dependencies UI から編集) 成果物 Pods/ + .xcworkspace を別管理 Xcode が直接管理 ロックファイル Podfile.lock Package.resolved Flutter での状況 長年の標準 3.44 でデフォルト有効化 Android側のしくみ AndroidのビルドはGradleが android/app/build.gradle.kts を読んで実行します。 概念 役割 buildTypes ビルド種別( debug / release ) productFlavors 同じアプリのバリアント定義。 applicationId やリソースを分岐できる applicationId / applicationIdSuffix アプリの一意な識別子。iOS の Bundle ID 相当 resValue ビルド時に文字列リソースを生成する Gradle の機能 AndroidManifest.xml アプリのメタデータ宣言。 android:label でアプリ名を指定 Gradleは、flavor別のリソース( AndroidManifest.xml や res/ 配下など)を source set という仕組みでディレクトリ単位に分けます。Gradleがflavorを選んだときに src/main/ と自動マージします。なお google-services.json は厳密にはsource set mergeの対象ではありません。google-servicesプラグインが src/<flavor>/google-services.json などのパスを優先度順に探索し、1ファイルを選びます。 Dart側のしくみ Dartでflavorを識別するにはコンパイル時定数を使います。 API / オプション 役割 --dart-define-from-file=path/to/x.json .json または .env ( KEY=VALUE 形式)で書いた値をまとめて Dart に渡す flutter コマンドのオプション String.fromEnvironment('KEY') 渡された値をコンパイル時定数として参照する API。 switch の case 値にもできる firebase_options.dart flutterfire configure が生成する Firebase 設定ファイル。通常は 1 ファイル = 1 Firebase プロジェクト 3層を貫く全体像 3層を1枚にまとめると、 app_config.yaml → mise run setup → 各層のファイル群 → flutter run --flavor X という流れです。以降、iOS・Android・Dartの順に、各層内で mise run setup と flutter run --flavor X がどのファイルへつながるかを示します。 iOS層では、xcconfigチェーンが Info.plist を解決します。 project.pbxproj のRun Scriptがflavor別の GoogleService-Info.plist をコピーします。 Android層では、 build.gradle.kts がflavorディレクトリの google-services.json と AndroidManifest.xml を結びつけます。 Dart層では、 flavor/{dev,stg,prd}.json の FLAVOR が起点になります。 firebase_options.dart が FLAVOR に応じて firebase_options_{dev,stg,prd}.dart を切り替えて返すコード( セレクタ shim )になります。 flutter run --flavor dev --dart-define-from-file=flavor/dev.json を1回実行したときに、各層で何が起きるかを並べると以下のようになります。 iOS :Scheme dev → Debug-dev.xcconfig で PRODUCT_BUNDLE_IDENTIFIER が .dev 付きに解決される。Build PhaseのRun Scriptが ios/firebase/dev/ のplistをコピーする。 Android :productFlavor dev が選ばれて applicationIdSuffix=".dev" が適用される。 src/dev/google-services.json がsource setにマージされる。 Dart : flavor/dev.json の FLAVOR=dev がコンパイル時定数になる。 firebase_options.dart のセレクタshimが firebase_options_dev.dart を返す。 ここまでがFlutterプロジェクトのビルド構成の概要です。これを踏まえて、本記事で取り組んだ課題と解決策を見ていきます。 背景・課題 3環境の設定整合が壊れやすい dev / stg / prdの3環境を切り替えるには、iOS・Android・Dartの3層を整合させる必要があります。各層の対象は次のとおりです。 iOS:Bundle ID、 GoogleService-Info.plist Android: applicationId 、 google-services.json Dart:Firebase初期化オプション 3層が独立しているため設定漏れによるビルド失敗やFirebase接続不可が繰り返し発生し、原因特定にも時間がかかっていました。 解決策の全体像 以下の5つの取り組みで課題を解決しました。 # 層 取り組み 1 iOS / 基盤 CocoaPods 撤去 → SPM 移行 2 iOS / flavor xcconfig × 9 Build Configuration + flavor 別 Firebase plist 3 Android / flavor productFlavors + flavor 別 google-services.json 4 Dart firebase_options.dart のセレクタ shim 化 5 setup app_config.yaml の per-flavor 化と mise 自動化 1つのflavorを選んで flutter run すると、iOS・Android・Dartの3層でそれぞれ対応する設定が適用されます。 実装内容と具体的な解決策 1. CocoaPodsを撤去してSPMに移行する CocoaPodsはRuby製のAppleプラットフォーム向けパッケージマネージャーで、Swift Package Manager(SPM)はApple公式の代替です。Flutter 3.44では新規プロジェクトでiOS・macOSともにSPMがデフォルトで有効化され、Firebaseをはじめとする主要プラグインもSPM対応を完了しています。 pod install が不要になるため、CIのセットアップも大幅に高速化されます。 CocoaPodsの統合を解除する 既存プロジェクトでは、 Podfile をいきなり削除するのではなく、まず pod deintegrate でXcodeプロジェクトからCocoaPodsの統合を解除します。 cd app/ios pod deintegrate # project.pbxproj から Pods 統合を除去 rm Podfile Podfile.lock # Podfile 本体を削除 rm だけで済ませてはいけません。 Podfile を削除しただけでは project.pbxproj にCocoaPods統合の痕跡が残ります。たとえば [CP] Check Pods Manifest.lock などのBuild Phaseや、 Pods-Runner xcconfigの #include 参照などです。 pod deintegrate はこれらを正しく除去します。 .gitignore も忘れず更新します。 # app/ios/.gitignore -.symlinks/ -Pods/ +# SPM +.build/ SPMが管理する Package.resolved はバージョンを固定するためリポジトリにコミットします。 Crashlytics dSYMのパスを更新する dSYM(Debug Symbols)はクラッシュレポートのスタックトレースを人間が読めるシンボルに復元するためのファイルです。Crashlyticsを使っている場合、そのdSYMをアップロードするBuild PhaseスクリプトをSPM構成のパスに合わせて更新します。このRun Scriptは setup 系スクリプトが自動生成するものではなく、Crashlyticsを利用する場合はプロジェクトごとに手動で追加する手順です。 Firebase公式のApple向けガイド は現在SPM構成を前提としており、Run Scriptに以下を追加するよう記載されています。 # Build Phases > Run Script " ${BUILD_DIR % /Build/* } /SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run " CocoaPods時の ${PODS_ROOT}/FirebaseCrashlytics/run に相当します。Xcode 15以降は User Script Sandboxing がデフォルトで有効になっています。有効な場合はRun ScriptのInput Filesに以下を列挙する必要があります( Firebase公式ドキュメント )。 ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME} ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME} ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist $(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist $(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH) # Debug Dylib(DEBUG_DYLIB)が有効な場合は追加で必要 ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}.debug.dylib 2. iOS:xcconfigでBuild Configurationを9パターンに整える iOSのXcodeプロジェクトは「Build Configuration」というビルド設定の単位を持ちます。各Configurationにはxcconfigファイル(テキストでビルド設定を記述する仕組み)を1つ紐付けられます。本テンプレートは、以下の9通りのBuild Configurationを用意しています。 dev stg prd Debug Debug-dev Debug-stg Debug-prd Profile Profile-dev Profile-stg Profile-prd Release Release-dev Release-stg Release-prd これらはあらかじめ project.pbxproj にコミット済みです。あわせて、それらを参照する dev / stg / prd の3 Schemeもコミット済みです。各Schemeは、Run/Test/Analyzeで Debug-<flavor> を参照します。Profileでは Profile-<flavor> 、Archiveでは Release-<flavor> を参照します。3つのSchemeには、Flutter標準の Run Prepare Flutter Framework Script pre-actionも含めて完全な状態で用意されています。このpre-actionは xcode_backend.sh prepare を実行します。これによりSPM統合に必要な FlutterGeneratedPluginSwiftPackage が解決されます。 mise run setup はこのScheme⇔Configurationのマッピングやpre-actionには一切触れません。書き換えるのは、各Configurationに紐づくxcconfigの値(Bundle IDやアプリ名など)だけです。 Flutter/Shared.xcconfig ← 全 Configuration 共通の値 Flutter/flavor-dev.xcconfig ← dev 固有の値(BUNDLE_ID_SUFFIX など) Flutter/flavor-stg.xcconfig Flutter/flavor-prd.xcconfig Flutter/Debug-dev.xcconfig ← #include で連結(9ファイル) Flutter/Debug-stg.xcconfig ... Flutter/Release-prd.xcconfig 後ろの #include が前の値を上書きするため、共通値( Shared )→ flavor固有値( flavor-dev )の順に重ねることで最終的な値が決まります。 Debug-dev.xcconfig は3行の #include のみで構成されます。 #include "Debug.xcconfig" #include "Shared.xcconfig" #include "flavor-dev.xcconfig" これでビルド時に対応するxcconfigが PRODUCT_BUNDLE_IDENTIFIER を解決します。その値が Info.plist 経由でflavor付きBundle ID(例: jp.testapp.dev )として適用されます。 Firebase plistをBuild Phaseで差し替える flavor別の GoogleService-Info.plist を使い分けるため、Build PhaseにRun Scriptを追加します。Run Script内の ${FLAVOR} は実行直前にxcconfigの値( dev / stg / prd )に置き換えられます。これによりコピー元ディレクトリがflavorごとに切り替わり、該当するplistだけが .app バンドルに入ります。 # Build Phases > Run Script SRC = " ${SRCROOT} /firebase/ ${FLAVOR} /GoogleService-Info.plist " DST = " ${BUILT_PRODUCTS_DIR} / ${PRODUCT_NAME} .app/GoogleService-Info.plist " if [ -f " $SRC " ]; then cp -f " $SRC " " $DST " else echo " warning: $SRC not found, Firebase native init will be skipped " fi plistが存在しないflavorでもビルドが失敗しないよう、存在チェックを入れています。 firebase_project_id を空にしてFirebase設定をスキップした場合がこれに該当します。このRun Scriptは読み書き対象のパスをInput Files / Output Filesとしてあらかじめ宣言しています。そのためUser Script Sandboxingが有効な環境でも安全に動作します。 plistは以下のスロットに配置します( setup:firebase が生成)。 app/ios/firebase/ ├── dev/GoogleService-Info.plist ├── stg/GoogleService-Info.plist └── prd/GoogleService-Info.plist 3. Android:productFlavorsとgoogle-services.jsonをflavorごとに配置する Androidでは build.gradle.kts に productFlavors を追加し、flavorごとに applicationIdSuffix とアプリ名を設定します。 // app/android/app/build.gradle.kts flavorDimensions + = "env" productFlavors { create( "dev" ) { dimension = "env" applicationIdSuffix = ".dev" resValue( "string" , "app_name" , "Flutter Template App Dev" ) } create( "stg" ) { dimension = "env" applicationIdSuffix = ".stg" resValue( "string" , "app_name" , "Flutter Template App Stg" ) } create( "prd" ) { dimension = "env" resValue( "string" , "app_name" , "Flutter Template App" ) } } google-services.json はAndroid Gradleのflavorソースセット( src/<flavor>/ )に配置します。flavor用の設定ファイルはgoogle-servicesプラグインが自動で選択します。 android/app/src/ ├── dev/google-services.json # setup:firebase が生成 ├── stg/google-services.json └── prd/google-services.json 注意点が2つあります。1つ目は google-services.json の package_name です。suffix込みの最終applicationIdに合わせます(例: com.example.app.dev )。suffixなしの com.example.app だけFirebaseに登録していると、dev / stgビルド時にプラグインが例外を投げてビルドが通りません。Firebase側のアプリ登録もsuffix込みのapplicationIdで行う必要があります。 AndroidManifest.xmlで android:label="@string/app_name" のように参照します。 productFlavors の resValue がflavorごとに生成した文字列が、そのままアプリ名として使われます。 <!-- app/android/app/src/main/AndroidManifest.xml --> <application android : label = "@string/app_name" ...> 2つ目は、 strings.xml との重複です。 resValue("string", "app_name", ...) はビルド時に文字列リソースを生成します。そのため、 res/values/strings.xml に同名の app_name が定義されていると Duplicate resources エラーになります。 app_name は strings.xml には置かず、各flavorの resValue に一本化してください。 4. Dart:firebase_optionsをセレクタshim化する flutterfire configure が生成する firebase_options.dart は単一の設定しか返しません。そこでファイル名とAPIを保ったまま、 セレクタ shim に置き換えます。 まず app/flavor/dev.json に FLAVOR キーを定義し、 --dart-define-from-file で読み込みます。 { " FLAVOR ": " dev " } FLAVOR はDartコード側で String.fromEnvironment('FLAVOR') を使って、コンパイル時定数として参照できます。 // app/lib/firebase_options.dart import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; import 'firebase_options_dev.dart' as dev; import 'firebase_options_prd.dart' as prd; import 'firebase_options_stg.dart' as stg; const _flavor = String .fromEnvironment( 'FLAVOR' ); class DefaultFirebaseOptions { static FirebaseOptions get currentPlatform { switch (_flavor) { case 'dev' : return dev.DefaultFirebaseOptions.currentPlatform; case 'stg' : return stg.DefaultFirebaseOptions.currentPlatform; case 'prd' : return prd.DefaultFirebaseOptions.currentPlatform; default : throw StateError( 'Unknown FLAVOR: "$_flavor". ' 'Run with --dart-define-from-file=flavor/<flavor>.json' , ); } } } 呼び出し側( main.dart )は DefaultFirebaseOptions.currentPlatform をそのまま使うだけで、flavorの切り替えを意識する必要がありません。 setup:firebase はこのファイルを上書きせず、本物の firebase_options_<flavor>.dart だけを再生成します。 注意:flavor指定は必須 _flavor が空( --dart-define-from-file 未指定)だとshimの default が StateError を投げます。 _initializeFirebase はこれをcatchしないため、flavorなしの flutter run は起動時に落ちます。VS Codeのlaunch.jsonや mise run タスクで各flavorを必ず渡す構成にしておくと安全です。 main.dartでduplicate-appを処理する Firebaseには2つの初期化経路があります。ネイティブの自動初期化と、Dart側の Firebase.initializeApp() です。両方が呼ばれると duplicate-app の FirebaseException が発生します。ただし duplicate-app はflavor不整合(ネイティブのplist/jsonが別のFirebaseプロジェクトを指している等)でも発生しうるため、無条件で成功扱いにはできません。ネイティブ初期化済みの options と比較し、一致した場合のみ成功扱いにします。不一致の場合は例外を再throwし、アプリを起動時にクラッシュさせます。例外を握りつぶしてしまうと、不正なAPIキーや設定不整合といった検知したいバグを見逃してしまう可能性があるためです。 // app/lib/main.dart(抜粋) Future< bool > _initializeFirebase() async { try { await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); return true ; } on UnimplementedError { // placeholder(setup:firebase 未実行)の場合は Firebase を無効化して続行 return false ; } on FirebaseException catch (e) { if (e.code == 'duplicate-app' ) { final existing = Firebase.app().options; final expected = DefaultFirebaseOptions.currentPlatform; final matchesNativeConfig = existing.appId == expected.appId && existing.projectId == expected.projectId && existing.apiKey == expected.apiKey; if (matchesNativeConfig) return true ; } rethrow ; } } 5. mise run setupで全flavorを一括生成する mise は、タスク実行とツールのバージョン管理を兼ねるCLIツールです。本テンプレートではセットアップスクリプトの実行に利用しています。手作業の設定漏れを防ぐため、セットアップの入力ファイル app_config.yaml にFirebaseのproject_idをflavorごとに設定できる構造を追加しました。これにより、 mise run setup:firebase で全flavorの設定ファイルを一括生成できます。 flavor : dev : app_name_suffix : " Dev" bundle_id_suffix : ".dev" firebase_project_id : "my-project-dev" # 空文字なら Firebase 設定をスキップ stg : app_name_suffix : " Stg" bundle_id_suffix : ".stg" firebase_project_id : "my-project-stg" prd : app_name_suffix : "" bundle_id_suffix : "" firebase_project_id : "my-project-prd" mise run setup:firebase は app_config.yaml の各flavorの firebase_project_id を読み取ります。読み取った値で flutterfire configure をflavorごとに実行します。 iOSは --ios-out でパスを直接指定すると、plistは出力されるもののproject.pbxprojへの参照は Runner/ 配下を前提に追加されます。配置と参照のズレでビルドが落ちる事例もありました。そこで setup:firebase は flutterfire configure のデフォルト出力( Runner/ )を利用し、後処理で配置を整えます。 Build PhaseのRun Scriptがflavorに応じてplistを動的にコピーするので、 project.pbxproj の静的参照は不要です。 setup:firebase は実行のたびに追記される参照を毎回除去し、pbxprojが肥大化しないようにしています。 # クリーンアップ確認(0 であれば OK) grep -c " GoogleService-Info.plist .*PBXBuildFile " \ app/ios/Runner.xcodeproj/project.pbxproj mise run setup:flavor はxcconfigやflavor JSONを app_config.yaml から再生成します。Firebaseの設定ファイルは別途 setup:firebase で更新します。何度実行しても同じ出力になるため、設定変更後に再実行すればxcconfigとflavor JSONが同期されます。 この仕組みにより、新規プロジェクトでは app_config.yaml を編集して mise run setup を実行するだけで済みます。iOS xcconfig・Android productFlavors・Dartセレクタshim・Firebase設定ファイルのすべてが一括でできあがります。 まとめ 本記事では、FlutterプロジェクトにおけるCocoaPodsからSPMへの移行を紹介しました。あわせてdev / stg / prdの3 flavorをiOS・Android・Dartの3層で整合させる仕組みの構築も紹介しました。 CocoaPodsからSPMへの移行自体は pod deintegrate → Podfile削除で思ったよりシンプルです。手間がかかるのはflavorの3層整合で、それぞれが独立した設定体系を持つため一つひとつ繋ぎ込む必要があります。 app_config.yaml → mise run setup という自動化の仕組みを整えました。これにより手動作業による設定漏れのリスクを排除し、チームが本来の開発に集中できる環境を作れました。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com




















