Resolve Solutions Logo Resolve Solutions
← All posts
12 min read

How I Designed Resolve Reinvent Flutter Architecture

A deep dive into the Dart-first, cross-platform architecture behind Resolve Reinvent: why I chose explicit state, built for iOS, Android, and web with minimal changes, and added a secure admin layer on top.

ResolveReinventResolveSolutionsFlutterDartArchitectureMVVMBasePagePatternViewModel
The primary goal of this application architecture was cross-platform delivery for iOS, Android, and web with virtually no changes.

I built Resolve Reinvent as a founder-led deep dive into how far a Dart-first architecture could go when I prioritized speed, clarity, and control.

I did not want to lock myself into a heavyweight framework. I wanted a practical Flutter architecture that felt explicit, debuggable, and flexible enough to ship a serious consumer app across mobile and web.

That led me to a pattern centered on ValueNotifier, ChangeNotifier, BasePage, and clear service boundaries. I can see where state lives, when it changes, and why the UI rebuilds.

See the backend architecture companion: How I Architected the ResolveAI Backend

Core Pattern: ValueNotifier + ViewModel

At the core, I use ValueNotifier for reactive global state, ValueListenableBuilder for focused widget updates, and ChangeNotifier + ListenableBuilder for page-level view models.

I also use manual dependency injection through factories like createViewModel(). It is lightweight, easy to debug, and removes framework lock-in.

Architecture Layers

The app is organized in four clear layers:

  • UI Layer: pages and widgets.
  • ViewModel Layer: business logic and view state.
  • Service Layer: API, auth, analytics, domain workflows.
  • Global State: shared notifiers, auth state, and caching signals.

BasePage Pattern

All major pages extend a BasePage<T extends BaseViewModel> template. That gives me a repeatable lifecycle contract and keeps page code consistent.

class HomePage extends BasePage<HomeViewModel> {
  @override
  HomeViewModel createViewModel() => HomeViewModel();

  @override
  Widget buildPage(BuildContext context, HomeViewModel viewModel) => ...;
}

The BasePage handles view model lifecycle and safety concerns for me:

  • initialize() and dispose() orchestration.
  • Loading and error wrappers.
  • Reactive ListenableBuilder wiring.
  • Page visibility hooks like onPageVisible() and onPageHidden().
  • AutomaticKeepAliveClientMixin so tabs stay alive in navigation stacks.

Widget Strategy

I use StatefulWidget and StatelessWidget intentionally, not dogmatically. Pages run through BasePage, reusable UI pieces are mostly stateless, and complex cards can own local state where needed.

I avoid accidental rebuilds. Updates are explicit through ListenableBuilder or ValueListenableBuilder.

Global State Without a Container Framework

Instead of app-wide provider scopes, I keep shared notifiers in a globals file with explicit ownership.

  • currentUser for auth state.
  • userProfileNotifier for profile updates.
  • themeModeNotifier for appearance.
  • caregiverLovedOneCountNotifier for cross-screen counters.

This works well for me because it is direct and transparent:

  • Single source of truth for shared signals.
  • Easy debugging through code search.
  • No hidden mutation layer.
  • Listeners can live at page or service boundaries.

The Admin Layer

I also added a secure admin layer because I needed a real operating console, not just a public app shell. It is protected by JWT tokens and built so I can manage the business without jumping between disconnected tools.

From that layer, I can:

  • Review engagement dashboards and core usage metrics.
  • Approve partners and manage partner workflow.
  • Ban users when needed and keep the platform healthy.
  • Monitor AWS spend and operational costs.
  • Track OpenAI usage so I can control token and model spend.
  • Use Firebase admin controls for backend operations.
  • Manage RevenueCat payment and subscription activity.

That admin surface matters to me because it turns the app from a collection of screens into something I can actually run, govern, and measure.

Service Layer: Domain Ownership

I built 40+ domain services that each own a clear slice of behavior: profile, auth, API, mood, journaling, notifications, analytics, conversation, avatar caching, and more.

Each service is responsible for:

  • Encapsulating domain rules.
  • Calling APIs through SecureApiService and JwtHttp.
  • Managing TTL-based caches.
  • Throwing domain-specific exceptions for clean UI handling.

State Patterns I Reuse Everywhere

Pattern one is cache-first plus stale-while-revalidate. I can return cached data immediately, then refresh in the background.

Pattern two is auth-driven invalidation. When currentUser changes, I can clear page caches, refresh profiles, and safely reset state.

Pattern three is reactive UI without setState noise. Pages listen to view models; components listen to focused notifiers.

Why I Chose This Architecture

The strengths are exactly what I needed for Resolve Reinvent:

  • No framework lock-in risk.
  • Explicit, inspectable state flow.
  • Strong multi-platform control for web, iOS, and Android.
  • Consistent error propagation from services to UI.
  • Good performance via targeted caching and controlled rebuilds.
  • Lifecycle safety around disposed view models.

I also accept the trade-offs:

  • More boilerplate per screen.
  • Manual dependency wiring.
  • Global notifier changes can cascade if not carefully managed.
  • Testing requires deliberate mocking of services and notifiers.

The Six Things I Treat as Non-Negotiable

  • Respect widget lifecycle: always unsubscribe listeners in dispose().
  • Respect cache contracts: cached profile can be stale, so check TTL.
  • Handle auth and profile notifiers as separate lifecycle events.
  • Route API traffic through JWT-aware services, not raw HTTP shortcuts.
  • Guard platform behavior with kIsWeb and platform checks.
  • Remember constants and config maps often require hot restart.

How AI Helped Me Ship This Faster

I do not describe this work as vibe coding. I describe it as supervised, AI-assisted engineering with architecture ownership staying human.

My workflow started with Claude Sonnet, moved heavily through Claude Opus 4.5 and 4.6, and most of the implementation volume was done with Opus 4.7 and 4.8. Right now, I am using a mix of Opus 4.8 plus auto mode with GPT-5, and that has worked well for token management and sustained delivery.

AI accelerated execution, but architecture, trade-offs, and production accountability stayed with me.

Flutter pubspec package map

These are the packages I treat as part of the app's practical runtime and delivery stack. I keep this list here because it shows the real shape of the product, but it is secondary to the architecture above.

Business-Critical App Spine

These packages are foundational to production runtime behavior.

  • flutter
  • http
  • flutter_dotenv
  • firebase_core
  • shared_preferences
  • intl
  • package_info_plus

Identity, Auth, and User Entry

This is the trust boundary and the start of the user lifecycle.

  • firebase_auth
  • google_sign_in
  • sign_in_with_apple
  • app_links
  • crypto

Revenue and Access Control

These are directly tied to premium features and the business model.

  • purchases_flutter
  • purchases_ui_flutter

Engagement and Retention Pipeline

This is how I bring users back and measure behavior.

  • firebase_messaging
  • flutter_local_notifications
  • firebase_analytics

Health + Context Features

These support the wellness side of Resolve.

  • health
  • geolocator
  • flutter_timezone

Voice and Audio Experience

These matter for journaling, guidance, accessibility, and habit loops.

  • speech_to_text
  • flutter_tts
  • just_audio
  • audio_session
  • permission_handler

Content, Media, and Sharing Workflows

These support user-generated content and interoperability.

  • image_picker
  • image
  • flutter_image_compress
  • share_plus
  • url_launcher
  • qr_flutter
  • flutter_markdown_plus
  • flutter_html

Visualization and UX Polish

Helpful and polished, but not core business logic.

  • fl_chart
  • syncfusion_flutter_gauges
  • lottie
  • flutter_colorpicker

Utility and Supporting Packages

Lower domain impact, but still useful infrastructure.

  • uuid
  • path_provider
  • cupertino_icons

Dev-Only Tooling

Not runtime logic, but essential for quality and release workflow.

  • flutter_test
  • flutter_lints
  • flutter_launcher_icons

Closing Thought

This architecture is not textbook MVVM and it is not framework fashion. It is a pragmatic system I designed for real product pressure: speed, clarity, debuggability, and long-term maintainability.

If you are building a serious Flutter app, explicit beats magical most days. That has been true for me in Resolve Reinvent, and it is a big reason I can keep shipping quickly without losing structural control.

On the backend, I do not force everything into one giant AWS monolith. The backend lives in a separate repo and follows the same AI-assisted workflow, with AWS doing the heavy lifting where it makes sense: API Gateway, Lambda, SQS, SNS, S3, and DynamoDB form the serverless backbone. I layer in Firebase, OpenAI, and other managed services when they add value, and that keeps the system practical instead of overengineered.

I think this is a strong pattern for small to mid-sized teams, and for larger companies that are trying to modernize. If you are sitting on native Android and iOS apps plus separate web teams, Flutter with Dart gives you a cleaner path to a single cross-platform codebase that still compiles down to native code. That makes it easier for smaller teams to control the product without fragmenting the frontend.

If you'd like to partner on a modernization effort, or if you need a technology lead, AI architect, evangelist, developer, or forward engineer, reach out to Marty at Resolve Solutions.

Want help designing resilient app architecture with AI in the loop?

Get in Touch