A multi-platform Flutter demo app built for Google Cloud Next 2026, showcasing Firebase AI and GenUI (Generative UI) in a personal-finance dashboard. Runs on Android, iOS, Web, and macOS from a single codebase.
- AI Financial Simulator — chat-powered insights using Firebase AI and structured output via Dartantic
- Generative UI — AI responses rendered as interactive Flutter widgets (charts, tables, chips) via GenUI
- Dashboard — sparkline cards, portfolio overview, and responsive layouts
- Rive Animations — loading screens and thinking indicators
- Multi-platform — runs on Android, iOS, Web, and macOS
- FVM (Flutter Version Management)
- FlutterFire CLI for Firebase configuration
fvm installThis project requires a Firebase project with Firebase AI and App Check enabled. The only config file needed is lib/firebase_options.dart (Dart-only initialization — no native config files required). Generate it with:
flutterfire configure --platforms=android,ios,macos,web --out=lib/firebase_options.dart# Development
fvm flutter run --target lib/main_development.dart
# Production
fvm flutter run --flavor production --target lib/main_production.dartTo enable App Check with reCAPTCHA (web builds):
fvm flutter run --target lib/main_development.dart --dart-define=RECAPTCHA_SITE_KEY=your_key_hereWorks on iOS, Android, Web, and macOS.
The app follows VGV's layered architecture with a feature-first structure and BLoC for state management:
lib/
app.dart — Root MaterialApp widget
app_check/ — Firebase App Check debug token helpers
bootstrap.dart — App initialization (Firebase, providers, error handling)
design_system/ — Theme, colors, spacing, and reusable UI widgets
dev_menu/ — Component catalog pages (development tool)
error_reporting/ — Error reporting repository
feature_flags/ — Feature flag repository, cubit, and dev menu drawer
l10n/ — Localization (English, Spanish)
onboarding/ — Intro, profile selection, and focus selection screens
simulator/ — AI Life Goal Simulator (the GenUI feature — see below)
Key dependencies:
- firebase_ai — Gemini model access via Firebase
- firebase_app_check — Protects Firebase backends from abuse
- genui — Renders structured AI output as Flutter widgets
- flutter_bloc — State management
- dartantic_ai / dartantic_firebase_ai — Structured output schemas for AI
The simulator/ directory is the heart of the app. It demonstrates how to integrate GenUI — where an LLM generates entire Flutter UIs as structured JSON — while keeping the complexity hidden behind clean architectural boundaries.
The key insight: the SimulatorRepository encapsulates all GenUI plumbing (catalog, surface controller, transport adapter, prompt builder, and chat model) behind a two-method API: startConversation() and sendMessage(). The bloc and UI layers never touch GenUI directly.
graph TD
presentation["<b>Presentation Layer</b> — simulator/view/
SimulatorPage → SimulatorView → SimulatorMessageBubble
Renders pages of messages and animates between pages"]
bloc["<b>Business Logic Layer</b> — simulator/bloc/
SimulatorBloc
Manages conversation state, loading, and navigation.
Subscribes to repository events. No GenUI or Firebase imports."]
repo["<b>Repository Layer</b> — simulator/repository/
SimulatorRepository
The boundary where GenUI lives. Owns Catalog, SurfaceController,
transport adapter, chat history, and prompt. Exposes a stream
of simple events and a SurfaceHost for rendering."]
data["<b>Data Layer</b> — simulator/catalog/ + simulator/prompt/
Finance Catalog — ~24 custom widgets registered as GenUI CatalogItems
Prompt Builder — system prompt with simulator persona,
conversation rules, and widget schemas"]
presentation --> bloc --> repo --> data
- User completes onboarding — profile type and focus areas flow into
SimulatorBlocviaSimulatorStarted. - Bloc calls the repository —
startConversation()wires up GenUI internals;sendMessage()sends the initial prompt. - Repository streams to the LLM — the system prompt (persona + widget schemas) and chat history go to
FirebaseAIChatModel. Responses stream back as chunks. - GenUI parses the response —
A2uiTransportAdapterextracts JSON describing surfaces and components.SurfaceControllerinstantiates widgets from the catalog. - Repository emits events — simple types like
SimulatorConversationSurfaceAddedandSimulatorConversationTextReceivedbubble up to the bloc. - Bloc updates state — each new surface becomes a page. The view animates to it and renders it via
SurfaceHost. - User interacts with a surface — input widgets (sliders, radio cards, filter chips) write values to GenUI's reactive
DataContext. When the user taps an action button, the interaction data is sent back to the LLM as the next message, and the cycle repeats.
| Directory | What to look at | Why |
|---|---|---|
simulator/repository/ |
SimulatorRepository |
Start here. This is the integration boundary — see how GenUI's catalog, controller, adapter, and chat model are composed and hidden behind a stream of simple events. |
simulator/bloc/ |
SimulatorBloc, SimulatorState |
See how repository events map to a paginated conversation state (List<List<DisplayMessage>>). Note: no GenUI imports. |
simulator/view/ |
SimulatorPage, SimulatorView, SimulatorMessageBubble |
See how surfaces are rendered and how page transitions are animated. The view only knows about SurfaceHost — it never constructs GenUI objects. |
simulator/catalog/items/ |
Any catalog item (e.g., gcn_slider_catalog_item.dart) |
See how a single widget is defined as a CatalogItem with a JSON schema and a widgetBuilder. This is how you add new components the LLM can use. |
simulator/prompt/ |
PromptBuilder |
See the system prompt that defines the simulator persona, conversation rules, and widget usage guidelines. This is where you shape the LLM's behavior. |
fvm dart run very_good_cli:very_good test --coverage --test-randomize-ordering-seed randomTo view the coverage report:
genhtml coverage/lcov.info -o coverage/
open coverage/index.htmlThis project uses bloc_lint to enforce best practices.
fvm dart run bloc_tools:bloc lint .You can also validate with VSCode using the official bloc extension.
This project uses flutter_localizations and follows the official internationalization guide.
Add strings to lib/l10n/arb/app_en.arb, then regenerate:
fvm flutter gen-l10n --arb-dir="lib/l10n/arb"Or just run the app — code generation happens automatically.

