04 - Architecture

The architecture in 2 minutes. For full component diagrams and sequence flows, see the Frontend Architecture HTML.

📈 Full diagrams elsewhere

This page is a quick-read summary. For interactive component diagrams, sequence diagrams, and UML-style architectural flows, open frontend_architecture.html in the project root. That file is the source of truth for visual architecture.

The 4 Layers

The app is a strict 4-layer architecture. Data flows top-to-bottom (higher layers depend on lower layers, never the reverse).

#LayerFolderResponsibility
1Presentation lib/screens/, lib/widgets/ Stage screens, wrapper widgets. UI rendering, user input, form validation, calling services.
2Service lib/services/ ApiService (backend calls), StorageService (persistence), AnalyticsService (events).
3Domain lib/models/ Plain data classes with fromJson factories. No logic.
4Configuration lib/config/ Compile-time constants: base URLs, routes, theme, asset paths.

Dependency Rule

Each layer only depends on layers below it.

If a screen imports another screen, something is wrong

Navigation is done via named routes (Navigator.pushReplacementNamed), not by importing the target screen class. This keeps screens decoupled.

Data Flow (in one sentence)

User interacts with a Screen, which calls an ApiService method, which HTTPs the Backend, which returns JSON that fromJson-factories into a Model, wrapped in ApiResponse<T>, and the screen does setState to re-render.

Why No State Management Library?

You might expect BLoC, Provider, or Riverpod. The app deliberately uses vanilla setState everywhere. Here's why:

eKYC is a linear journey, not a reactive app

Each stage is a self-contained screen. State does not need to be shared across 5 screens at once. setState is sufficient. No BLoC boilerplate, no provider tree, no stream subscriptions. Fewer files, easier to debug.

Shared state that does cross screens (leadId, token, sessionId) lives in the StorageService singleton.

Cross-Cutting Concerns

Loading spinners, session timeouts, back button handling, progress bars - these are handled by wrapper widgets, composed around screen content.

Typical screen wrapper compositionDart
return BackButtonHandler(
  child: SessionTimer(
    child: Scaffold(
      appBar: AppBar(title: const Text('Bank Verification')),
      body: LoadingOverlay(
        isLoading: _isLoading,
        message: 'Verifying...',
        child: Column(
          children: [
            const JourneyProgressBar(currentStage: 6),
            _buildBody(),
          ],
        ),
      ),
    ),
  ),
);

Each wrapper is independent and optional. If Stage 0 doesn't need back button handling, just omit BackButtonHandler. No mixin, no inheritance, no base class.

Why Single ApiService?

Instead of one repository per feature (common in BLoC-based apps), the entire backend is exposed through a single ApiService class with ~60 methods.

Trade-off

Pro: All endpoints discoverable in one file. Auth, headers, error handling, stage tracking are centralized. New developers only need to read one file to understand the API surface.
Con: File is large (~1100 lines). Mitigated by grouping methods with section comments per stage.

Where Logic Lives

WhatWhere
Form validation (client-side)Screen file (e.g. _validateMobile)
API call with auth + error handlingApiService method
Persistent state (leadId, token)StorageService singleton
Transient UI state (loading, view state)Screen's State<T> class (setState)
NavigationNavigator.pushReplacementNamed via routes in AppRoutes
Theme / colors / fontslib/config/theme.dart
Environment-specific URLslib/config/api_config.dart (compile-time)
Mock URL detection (dev only)Inline in Stage 5 and Stage 6 screens, guarded by ApiConfig.isDev
Business logic decisionsMostly backend. Frontend mirrors backend responses.

Full Diagrams

Open these HTML files in your browser for the interactive visual architecture: