The eKYC 3.0 frontend is a Flutter application built once in Dart and compiled natively to Android, iOS, and Web. It provides a 16-stage digital onboarding journey for opening Demat accounts.
Framework
Flutter 3.2+, Dart 3.x, Material 3
State
Vanilla setState, no BLoC/Provider
Network
http package, JWT bearer auth
Storage
SharedPreferences singleton
Animations
Lottie + Material transitions
Assets
SVG (flutter_svg) + PNG + Lottie JSON
2 Component Diagram - Full System View
How all components in the eKYC frontend connect with each other and with external services. Boxes represent components, lines represent dependencies/interactions.
Solid lines = direct calls
Stage screens directly call ApiService and StorageService methods.
Dashed lines = external boundaries
HTTPS to backend, platform channels to native storage, deeplinks to external providers.
3 Component Sequence Diagrams
These diagrams show architectural component interactions - how the layers and classes talk to each other, independent of any specific business flow. Use these to understand the wiring of the app.
3.1 - API Call Pipeline (how any screen talks to the backend)
Every network call in the app follows this same pattern, regardless of stage or method. This diagram shows the internal component chain from a Screen to the backend and back.
This is the ONLY network path in the app
Every single API call goes through this same sequence. The only thing that changes per method is the URL and the fromJson factory. Auth, headers, error handling, stage tracking are centralized in _post/_get/_put.
3.2 - Reactive State Update Loop (Flutter widget + state)
How a user event triggers a UI rebuild. This is the mental model for vanilla setState - no external state library involved.
Why this is enough (no Redux/BLoC/Provider)
Flutter's framework is already a reactive system. setState() schedules a rebuild, Flutter diffs the widget tree, and repaints only the changed parts. For a linear journey app like eKYC, this loop handles 100% of state needs.
3.3 - Wrapper Widget Composition Chain
How cross-cutting concerns (back button, session timeout, loading) are layered as wrapper widgets without inheritance or mixins.
Composition over inheritance
No base class. No mixins. Each wrapper is an independent widget with a single child property. You can add/remove wrappers per screen without touching other code. Flutter's Element tree resolves the chain at build time.
How multiple services/screens share the same session data via the StorageService singleton.
Why singleton?
The Flutter widget tree is stateful per-screen, but leadId, token, sessionId must survive screen transitions and be accessible from any ApiService instance. A singleton gives us one source of truth for session state without DI frameworks.
3.5 - Build-Time Config Resolution
How the compile-time environment flag flows through components. No runtime lookups, no config files - everything is a Dart constant.
Tree-shaking guarantees production safety
Because _env is a compile-time constant, the Dart compiler evaluates all if (ApiConfig.isDev) branches at build time. In a prod build, these are dead code and get stripped. There is no runtime way to force mock mode or switch to a dev URL.
4 Layered Architecture
The app follows a clean layered architecture with strict unidirectional data flow. Each layer has a single responsibility and depends only on layers below it.
Unidirectional Dependency
Screens depend on Services. Services depend on Models. Models depend on nothing. No circular references.
Single Responsibility
Each layer has one job. Screens render UI, Services talk to backend, Models hold data, Config holds constants.
5 Code-Level Architecture Patterns
The codebase deliberately uses simple, well-known patterns that any Flutter developer can pick up. No heavyweight frameworks.
A 4-layer separation: Presentation → Service → Domain → Config. Each screen calls ApiService, gets back typed ApiResponse<T>, displays the result. No business logic in widgets.
2. Repository (Single Service Aggregator)
All 60+ API methods are consolidated in ApiService. Instead of one repository per feature (BLoC pattern), one class with named methods. Easier to discover, single point of authentication and error handling.
3. Singleton Storage
StorageService.getInstance() returns a single SharedPreferences-backed instance. All screens share the same lead/session/token state. Async-initialized once at app boot.
4. State Machine UI
Complex screens (Stage 6 Bank, Stage 13 eSign) use Dart enum view-states. Example: enum _BankViewState { preVerified, upi, manual, verified }. The build method switches on the enum to render the right UI.
5. Wrapper Widget Composition
Cross-cutting concerns (loading, session timeout, back button) are implemented as wrapper widgets that screens compose: BackButtonHandler > SessionTimer > LoadingOverlay > ScreenContent. No mixins, no inheritance.
6. Result Type (ApiResponse<T>)
Every API call returns ApiResponse<T> with success, data, errorCode, displayMessage. No exceptions for control flow. Screens check result.success and act accordingly.
7. Build-Time Environment Config
String.fromEnvironment('ENV') reads --dart-define=ENV=prod at build time. Compile-time constant, tree-shakeable, no runtime config files. Three envs: dev/uat/prod.
8. Backend-Driven Mock Detection
Flutter doesn't know about mock mode. Backend sets GlobalMockEnabled=true and returns redirect URLs containing example.com. Flutter detects these URLs and skips external launches in dev only (guarded by ApiConfig.isDev).
6 State Management - Why Vanilla setState?
The new app uses plain setState instead of BLoC, Provider, Riverpod, or any state management library. Here's why and how it's structured.
Why no BLoC?
Aspect
BLoC / Provider
Vanilla setState (chosen)
Boilerplate
3 files per feature (cubit, state, event)
1 file per screen
Learning Curve
High - new mental model
Built into Flutter
State scope
App-wide reactive streams
Local to widget tree
Best for
Complex apps with shared state across many screens
Linear journeys where each screen owns its state
eKYC fit
Overkill - screens don't share state mid-journey
Perfect - each stage is isolated
7 Network Layer
Single ApiService class wraps the http package with auth, error handling, session tracking, and a typed result wrapper.
Static named routes via MaterialApp.routes. No GoRouter, no navigator 2.0. Stage progression uses pushReplacementNamed to prevent back-stack accumulation.
// Forward only - no back stack
Navigator.pushReplacementNamed(
context,
AppRoutes.stage7Liveness,
);
// Resume from any stage on app launchfinal route = AppRoutes
.stageRoutes[currentStage]
?? AppRoutes.stage1Registration;
Why pushReplacementNamed?
eKYC is a one-way journey. The user must not press "Back" and undo Stage 6 after Stage 7. Each stage replaces the previous in the stack. Real back-button is intercepted by BackButtonHandler wrapper widget.
9 Widget Composition
Cross-cutting concerns are wrapper widgets that compose around screen content. No mixins, no inheritance hierarchies.
String.fromEnvironment resolves at build time. Dead code paths (e.g., dev URLs in a prod build) are removed by the Dart compiler. No risk of dev URLs leaking into prod APKs.
11 Platform Build Pipeline
Single Dart codebase → Flutter compiler → native binaries for each platform.
12 Mock Mode Strategy
The frontend is completely unaware of mock mode. All mock behavior is driven by the backend's GlobalMockEnabled flag. The frontend only detects mock URLs in responses and bypasses external launches.
Why backend-driven, not frontend flag?
A Flutter mock flag could be tampered with at runtime (jailbreak/root). Backend-driven mock means the frontend has no way to "force mock mode" against a prod backend. Production builds compile away the dev URL guards entirely (ApiConfig.isDev is a compile-time constant).
13 Annexures: Platform Build Details
Platform-specific build configurations, commands, permissions, and plugin integration details. Click to expand each section.
A Android Build Details
Build Configuration
Setting
Value
Min SDK
21 (Android 5.0 Lollipop)
Target SDK
34 (Android 14)
Build Tool
Gradle (AGP 8.x)
Kotlin Version
1.9+
Java Version
17 (recommended)
Architectures
arm64-v8a, armeabi-v7a, x86_64
Output
APK / AAB (Android App Bundle)
Build Commands
# Debug APK
flutter build apk --debug
# Release APK (split per ABI)
flutter build apk --release \
--split-per-abi \
--dart-define=ENV=prod
# App Bundle for Play Store
flutter build appbundle --release \
--dart-define=ENV=prod \
--obfuscate \
--split-debug-info=build/symbols
Native Components
Flutter generates a thin Android wrapper. Plugins that need native code (camera, geolocator, image_picker, signature, razorpay_flutter) include their own AndroidManifest entries and Gradle dependencies via the Flutter plugin system.
# Debug build (simulator)
flutter build ios --debug --simulator
# Release IPA for TestFlight
flutter build ipa --release \
--dart-define=ENV=prod \
--export-options-plist=ios/ExportOptions.plist
# Open in Xcode for signing
open ios/Runner.xcworkspace
Info.plist Permissions
<key>NSCameraUsageDescription</key>
<string>Required for selfie capture and document upload</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Required to verify your location during onboarding</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Required to upload PAN, Aadhaar, and bank proof documents</string>
Native Plugin Integration
CocoaPods auto-installs native iOS dependencies for plugins. Run cd ios && pod install after adding new Flutter packages.