Files
cross-plat-design/DESIGN.md
T

22 KiB
Raw Blame History

Production-Ready Flutter Cross-Platform UI Architecture

Overview

This document defines the architecture for a maintainable Flutter application targeting Windows, Linux, macOS, Android, iOS, and Web.

Selected stack: Flutter + Forui + Riverpod

The architecture combines a shared design system, feature-first modules, adaptive application shells, and Riverpod-based application state. Desktop, mobile, and browser interfaces share product logic and visual standards without being forced to look or behave identically.

1. Architecture Decisions

Area Decision
UI foundation Forui
Application framework Flutter
State management Riverpod, with code generation where beneficial
Routing go_router
Architecture Feature-first, with presentation, domain, and data layers where justified
Responsive design Constraint-based layouts and centralized breakpoints
Theming Semantic design tokens and Forui themes
Forms Shared form components and validation rules
Platform differences Separate shell/navigation implementations where needed
Testing Unit, widget, integration, and platform-specific smoke tests

Key architectural rule

Forui supplies the visual component foundation; the product design system supplies application standards. Riverpod manages state and dependency injection; it must not contain widget styling or layout decisions.

2. Version and Dependency Policy

Forui is actively maintained and provides more than 50 components, theme generation, localization support, and widgets for desktop and touch interfaces. Its documentation currently lists version 0.27.6; that release requires a sufficiently recent Flutter and Dart SDK.

Before starting, verify the installed SDK:

flutter --version
flutter doctor -v

Plan around Flutter 3.47 or newer for the current Forui release, and confirm the SDK constraints in package metadata before locking dependencies. Do not upgrade Flutter blindly if other applications depend on an older version.

Use flutter pub add to resolve versions compatible with the installed SDK instead of manually guessing versions for transitive packages.

flutter pub add forui
flutter pub add flutter_riverpod
flutter pub add riverpod_annotation
flutter pub add go_router
flutter pub add shared_preferences
flutter pub add path_provider
flutter pub add file_selector

flutter pub add --dev riverpod_generator
flutter pub add --dev build_runner
flutter pub add --dev riverpod_lint
flutter pub add --dev custom_lint
Dependency Purpose
forui UI components, themes, overlays, and navigation components
flutter_riverpod State management and dependency injection
riverpod_annotation Optional provider code generation
go_router Named routes, nested navigation, browser URLs, and deep links
shared_preferences Lightweight preferences such as theme and density
path_provider Platform-specific application directories
file_selector Cross-platform file selection
riverpod_generator Generates Riverpod provider code
build_runner Runs Dart code generators
riverpod_lint, custom_lint Enforces Riverpod coding practices

Riverpod separates application logic from UI and exposes loading, error, and data states explicitly. go_router supports all six target platforms and URL-based navigation.

Do not install a package merely because it is popular. Add database, networking, Markdown parsing, syntax highlighting, and persistence packages only when the application requires them. Keep the dependency count small, review licenses, and commit pubspec.lock for the application.

3. Folder Architecture

Use lib/ as the production source root. Keep feature-specific UI near its feature, shared design components under design_system/, and platform shells under app/shells/.

lib/
├── main.dart
│
├── app/
│   ├── app.dart
│   ├── app_router.dart
│   ├── app_providers.dart
│   ├── app_lifecycle.dart
│   ├── bootstrap.dart
│   └── shells/
│       ├── app_shell.dart
│       ├── desktop_shell.dart
│       ├── mobile_shell.dart
│       ├── tablet_shell.dart
│       └── web_shell.dart
│
├── core/
│   ├── config/
│   │   ├── app_config.dart
│   │   └── environment.dart
│   ├── errors/
│   ├── logging/
│   ├── platform/
│   │   ├── platform_capabilities.dart
│   │   └── platform_policy.dart
│   ├── responsive/
│   │   ├── breakpoints.dart
│   │   ├── layout_class.dart
│   │   ├── responsive_builder.dart
│   │   └── window_constraints.dart
│   ├── accessibility/
│   └── utils/
│
├── design_system/
│   ├── tokens/
│   │   ├── colors.dart
│   │   ├── spacing.dart
│   │   ├── typography.dart
│   │   ├── radii.dart
│   │   ├── dimensions.dart
│   │   ├── elevation.dart
│   │   └── motion.dart
│   ├── theme/
│   │   ├── app_theme.dart
│   │   ├── theme_mode_provider.dart
│   │   ├── theme_extensions.dart
│   │   └── generated/
│   ├── components/
│   │   ├── buttons/
│   │   ├── fields/
│   │   ├── navigation/
│   │   ├── dialogs/
│   │   ├── menus/
│   │   ├── feedback/
│   │   ├── surfaces/
│   │   ├── lists/
│   │   ├── tables/
│   │   └── loading/
│   ├── layouts/
│   │   ├── page_container.dart
│   │   ├── content_constraint.dart
│   │   ├── split_view.dart
│   │   └── section_layout.dart
│   └── icons/
│
├── shared/
│   ├── models/
│   ├── services/
│   ├── repositories/
│   ├── extensions/
│   └── widgets/
│
├── features/
│   ├── home/
│   │   ├── domain/
│   │   ├── data/
│   │   └── presentation/
│   │       ├── screens/
│   │       ├── widgets/
│   │       └── providers/
│   ├── editor/
│   │   ├── domain/
│   │   ├── data/
│   │   └── presentation/
│   │       ├── screens/
│   │       ├── widgets/
│   │       └── providers/
│   ├── preview/
│   ├── documents/
│   ├── search/
│   ├── settings/
│   └── about/
│
├── l10n/
└── generated/

Feature module convention

When a feature grows, use this internal structure:

features/editor/
├── domain/
│   ├── entities/
│   ├── repositories/
│   └── use_cases/
├── data/
│   ├── datasources/
│   ├── models/
│   └── repositories/
└── presentation/
    ├── screens/
    ├── widgets/
    └── providers/
        ├── editor_state.dart
        ├── editor_state.g.dart
        └── editor_controller.dart

Do not create empty domain/, data/, or use_cases/ folders for every small feature. Introduce those layers when there is actual business logic, data persistence, or an external source to abstract.

Architecture boundaries

  • design_system/ may depend on Flutter and Forui, but must not depend on product features.
  • features/ may use design-system components and Riverpod providers.
  • app/ composes features, routing, global providers, and shells.
  • core/ contains infrastructure and platform policies, not feature-specific UI.
  • Domain logic must not import Flutter widgets or Forui.
  • Generated files are outputs; never edit .g.dart files manually.

4. Design Tokens and Visual Standards

Use semantic tokens instead of raw hex values scattered throughout widgets. Start with a neutral, restrained interface, then customize the Forui theme using its theme tooling. Forui provides a built-in neutral theme and a visual theme generator.

These defaults are design-system values, not mandatory platform specifications.

Token Value
Base spacing 4 logical px
space1 4
space2 8
space3 12
space4 16
space6 24
space8 32
space12 48
Small radius 6
Standard radius 10
Large radius 14
Standard border 1
Standard icon 20
Compact control height 36
Standard control height 40
Comfortable control height 44–48

Color palette

Role Starting value Use
Primary #2563EB Actions, links, and selection
Light background #F8FAFC Light-mode page canvas
Dark surface #111827 Dark-mode panels
Border #E2E8F0 Light-mode separators

Define semantic roles for background, surface, surfaceElevated, textPrimary, textSecondary, border, primary, primaryForeground, success, warning, danger, and focus. Define corresponding dark-mode roles rather than simply inverting every color.

Token implementation

abstract final class AppSpacing {
  static const double x1 = 4;
  static const double x2 = 8;
  static const double x3 = 12;
  static const double x4 = 16;
  static const double x6 = 24;
  static const double x8 = 32;
  static const double x12 = 48;
}

abstract final class AppDimensions {
  static const double sidebarExpanded = 248;
  static const double sidebarCollapsed = 64;
  static const double desktopToolbar = 44;
  static const double mobileToolbar = 56;
  static const double minimumTouchTarget = 48;
  static const double maxReadingWidth = 800;
}

Forui theme colors and component styles are the source of truth for Forui widgets. Use custom tokens for application-level spacing, content constraints, editor typography, and components Forui does not cover.

Typography rules

  • Use a consistent system sans-serif font for UI text.
  • Start body text at 14–16 logical px.
  • Do not reduce secondary text below comfortable readability merely to fit more information.
  • Give editor text a separate configurable reading scale, with optional monospace styling for code.
  • Preserve user text scaling; avoid fixed-height text containers that clip enlarged text.
  • Use weight and spacing for hierarchy before adding more colors.

5. Responsive Shell Architecture

Flutter distinguishes responsive design, which fits available space, from adaptive design, which chooses appropriate interactions and layouts. Use both.

The shell makes three decisions:

  1. Available width: compact, medium, expanded, or wide layout.
  2. Platform capabilities: desktop keyboard/mouse, mobile touch, browser URLs, and file access.
  3. Feature needs: whether the current screen benefits from a split view, sidebar, bottom navigation, or full-screen content.

Breakpoint policy

Layout class Available width Default layout
Compact < 600 One main pane; bottom navigation or compact app bar
Medium 600–839 Navigation rail or drawer; optional secondary pane
Expanded 840–1199 Persistent sidebar; main content
Wide 1200+ Sidebar plus optional secondary panel

These thresholds are starting points. Base decisions on LayoutBuilder constraints rather than physical device model or orientation alone.

Shell selection

Shell Responsibilities
AppShell Routing, theme, global shortcuts, and app state
DesktopShell Window-aware sidebar, toolbar, and split panes
MobileShell Touch-first navigation and compact actions
TabletShell Rail navigation and adaptive panels
WebShell URL-aware navigation and browser-friendly behavior

The web shell does not need to duplicate the desktop shell. At a wide browser viewport it can reuse desktop workspace components while retaining web routing, browser history, and browser-specific behavior.

Breakpoint implementation

enum LayoutClass {
  compact,
  medium,
  expanded,
  wide,
}

LayoutClass layoutClassFor(double width) {
  if (width < 600) return LayoutClass.compact;
  if (width < 840) return LayoutClass.medium;
  if (width < 1200) return LayoutClass.expanded;
  return LayoutClass.wide;
}
class ResponsiveShell extends StatelessWidget {
  const ResponsiveShell({
    super.key,
    required this.child,
  });

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final layout = layoutClassFor(constraints.maxWidth);

        return switch (layout) {
          LayoutClass.compact => MobileShell(child: child),
          LayoutClass.medium => TabletShell(child: child),
          LayoutClass.expanded => DesktopShell(child: child),
          LayoutClass.wide => DesktopShell(
              child: child,
              showSecondaryPanel: true,
            ),
        };
      },
    );
  }
}

This is a layout-policy example. Implement shell widgets separately and add platform policy for behavior that differs between a native desktop application and a browser. For example, do not display a desktop window title bar in the browser merely because the viewport is wide.

6. Platform-Specific Standards

The six targets share design tokens, feature logic, and most components. They do not necessarily share exact navigation or input behavior. Platform-specific rules are maintained separately:

Use these documents alongside the shared responsive shell policy. Platform rules should describe behavior that genuinely differs by target; shared visual standards remain in this document.

7. Reusable Component Catalogue

Create a small, consistent set of product components on top of Forui. Feature screens should use these components instead of independently choosing styles for the same task.

Component Responsibilities
AppButton Primary, secondary, outline, ghost, and destructive variants
AppIconButton Consistent sizing, tooltip, focus, and accessibility label
AppTextField Label, helper text, validation, and disabled state
AppSearchField Search input, clear action, and loading state
AppDialog Confirmation and form dialogs
AppSheet Mobile-friendly secondary actions and forms
AppToast Brief success and non-blocking feedback
AppEmptyState Message, optional illustration/icon, and next action
AppErrorState Human-readable error and recovery action
AppLoadingState Loading indicator and optional supporting text
AppPageHeader Title, description, primary actions, and relevant breadcrumbs
AppSidebar Navigation destinations, selected state, and collapse behavior
AppResponsiveContainer Content width and horizontal padding
AppSplitView Main content and secondary panel on larger windows
AppSection Consistent heading and content spacing
AppStatusBadge Semantic status labels, not decorative status colors

Component rules

  • Use Forui components where they already meet application standards.
  • Wrap a component only when a stable application-wide API or consistent behavior is needed.
  • Do not build a custom replacement for every Forui component.
  • Keep button variants and status colors limited.
  • Every interactive component needs disabled, focus, hover where relevant, and loading behavior.
  • Every asynchronous feature needs clear loading, success, empty, and error states.
  • Do not introduce a custom component merely to rename a single Forui widget.

Forui already provides controls, forms, navigation, cards, overlays, dialogs, sheets, tooltips, and feedback widgets. Use its documented catalogue before adding another UI library.

8. Riverpod State-Management Standards

Organize providers by feature and responsibility. Avoid one giant app_state.dart file that owns unrelated screens.

State ownership

State Recommended owner
Theme preference App-level Riverpod provider plus preference storage
Navigation state and current route go_router
Sidebar expanded/collapsed Shell-level state
Editor content Editor feature controller
Search query and filters Feature-level provider or local widget state
Loaded documents Repository plus Riverpod provider
Loading/error/data states Riverpod asynchronous providers
Temporary input text Text controller or local widget state
Window size and layout class Flutter layout constraints, not persisted global state

Provider conventions

  • Use ConsumerWidget or ConsumerStatefulWidget when a widget needs Riverpod.
  • Use ref.watch to render state and ref.read for event-driven commands.
  • Use select when only one part of a larger state is needed.
  • Keep widget build methods focused on presentation.
  • Keep repository implementations outside UI widgets.
  • Use AsyncValue to model asynchronous loading, success, and failure.
  • Use code generation where it reduces boilerplate; do not require it for every trivial provider.
  • Avoid global mutable singletons that bypass Riverpod.

Example provider organization:

features/editor/presentation/providers/
├── editor_state.dart
├── editor_state.g.dart
├── editor_controller.dart
├── editor_document_provider.dart
└── editor_shortcuts.dart

Use the Riverpod documentation as the reference for provider lifecycles, asynchronous state, testing, and code generation.

9. App Startup, Routing, and Theming

Use ProviderScope at the root, MaterialApp.router with go_router, and Forui's FTheme in the app builder. Follow the installed Forui version's integration pattern because Forui is pre-1.0 and may introduce API changes between minor releases.

The application should have one theme-selection policy:

  1. Read the saved theme preference: system, light, or dark.
  2. Resolve the current brightness.
  3. Select the corresponding Forui theme.
  4. Apply that theme consistently across all routes and overlays.
  5. Update the preference when the user changes it.

Use the Forui theme generator to create actual theme definitions rather than inventing a theme constructor from memory.

Use go_router as the single source of truth for navigation state and current route state. Organize routes centrally and use nested routes for features that share a shell. Keep browser URLs meaningful on Web, and ensure back-navigation behavior is correct on Android and iOS. Do not duplicate navigation state in Riverpod providers or global mutable state.

10. Testing and Quality Gates

A cross-platform project is not production-ready merely because it compiles on all six targets. Test each shell and every capability that differs by platform.

Test level What to verify
Unit tests Breakpoints, validation, repositories, and domain logic
Widget tests Shared components, themes, and loading/error/empty states
Responsive tests Compact, medium, expanded, and wide layouts
Accessibility tests Labels, focus, contrast, and text scaling
Integration tests Routing, document workflows, and persistence
Desktop smoke tests Resize, shortcuts, context menus, and file selection
Mobile smoke tests Safe areas, keyboard, back navigation, and lifecycle
Web smoke tests Refresh, direct URLs, browser history, downloads, and file selection

Minimum release checks

dart format --output=none --set-exit-if-changed .
flutter analyze
flutter test

Also:

  • Build and launch each supported target.
  • Test light and dark themes.
  • Test narrow and wide layouts.
  • Verify failed asynchronous operations display recoverable errors.
  • Check that important text is not clipped at larger system text scales.
  • Test file access separately on every platform where it is supported.

Use CI for formatting, static analysis, unit/widget tests, and platform builds supported by runners. Keep manual or automated smoke tests for platform-specific behavior.

11. Implementation Sequence

  1. Create the project and validate all targets. Check the Flutter SDK, enable required desktop targets, and confirm baseline builds before adding dependencies.
  2. Install and lock dependencies. Add Forui, Riverpod, routing, and only required storage/platform packages.
  3. Build the design system. Define tokens, typography, light/dark themes, and component conventions.
  4. Build shared components. Implement standard buttons, fields, feedback states, dialogs, and page containers.
  5. Implement routing and global state. Add ProviderScope, theme preference, route configuration, and application-level error handling.
  6. Implement responsive shells. Start with compact, medium, expanded, and wide layouts. Add platform-specific behavior only where it provides a real benefit.
  7. Build one representative feature. Use a real screen with forms, navigation, asynchronous data, and error states to validate the architecture before scaling it.
  8. Expand feature by feature. Keep business logic separate from UI and introduce data/domain layers when justified.
  9. Validate all six platforms. Test keyboard and touch interactions, browser behavior, file access, text scaling, and window resizing.
  10. Release with quality gates. Automate analysis and tests, document dependency upgrades, and test Forui upgrades before adopting them.

Final Recommendation

Use Forui for components, Riverpod for state and dependency injection, go_router for navigation, and semantic design tokens for visual consistency. Build one shared feature layer with adaptive shells instead of six separate UIs.

The most important decision is to stabilize the design system before building many screens. If every feature independently decides its colors, spacing, dialogs, and navigation behavior, the application will become inconsistent even when individual screens look acceptable.

References