Phase 08

Mutation Controller Architecture (login_controller.dart)

Calculating reading time... Module 2: Transwave Deep Dives

🎯 Senior Engineering Objective

Master production mutation controller architecture in Riverpod. Understand why controllers exist as transaction orchestration layers between UI widgets and data repositories, how to structure multi-stage async pipelines, handle non-fatal exceptions, and refresh dependent providers.

1. Why Does This File Exist? (The Concept of Orchestration)

In simple apps, logging in means calling a single API endpoint. In production enterprise applications, logging in is a multi-transaction orchestration pipeline requiring 4 distinct steps:
1. Firebase SDK Authentication
2. Backend Database Sync (`POST /users/sync`)
3. FCM Push Notification Token Registration
4. Invalidation & Refresh of global cached state (currentUserProvider)

🌊 Why Repositories Must Never Expose UI State

Repositories are stateless data mappers responsible strictly for raw HTTP/Database calls. They must NEVER own UI loading states, show Snackbars, or perform page navigation.
The Mutation Controller exists to bridge UI widgets and data repositories. It owns the AsyncValue<void> state, handles loading spinners, captures exceptions, and orchestrates multi-step transactions.

2. Who Uses This File? (Codebase Search)

Consumer File Consumption Pattern Purpose
lib/features/auth/screens/login_screen.dart ref.watch(loginControllerProvider) Renders loading spinner on submit button during auth pipeline execution.
lib/features/auth/screens/login_screen.dart ref.read(loginControllerProvider.notifier).login(email, pass) Invokes email/password login pipeline on form submission.
lib/features/auth/screens/login_screen.dart ref.read(loginControllerProvider.notifier).loginWithGoogle() Invokes Google OAuth login pipeline.

Special Section A: Complete Mutation Pipeline Timeline

Multi-Step Transaction Pipeline Sequence
1
User Submits Form in LoginScreen
State transitions to AsyncLoading(). UI disables input fields and displays loading indicator.
2
Transaction 1: Firebase Authentication
Executes repo.signInWithEmail(email, password). Authenticates credentials with Firebase.
3
Transaction 2: Backend User Sync
Executes repo.syncUser() (`POST /users/sync`). Ensures user record exists in PostgreSQL backend database.
4
Transaction 3: Non-Fatal FCM Token Registration
Retrieves FCM push token and calls registerDeviceWithFcm(). Exceptions are caught and ignored as non-fatal.
5
Transaction 4: Profile Cache Refresh
Executes ref.read(currentUserProvider.notifier).refresh() to fetch fresh user profile data.
6
State Resolves to AsyncData(void)
authStateProvider stream triggers GoRouter to automatically redirect to `/home`.

Special Section B: Visual Orchestration Architecture

1. Presentation UI (LoginScreen)
LoginButton
Triggers login() callback
↓ Invokes Notifier Action Method
2. Orchestration Controller (LoginController)
state = AsyncLoading
Disables submit button
AsyncValue.guard()
Transaction pipeline wrapper
↓ Orchestrates Repositories & Downstream Providers
3. Dependencies & External SDKs
AuthRepository
Firebase & Backend HTTP
currentUserProvider
Refreshes cached profile

Special Section C: Failure Flow & Exception Strategy

Failure Scenario Exception Type Handling Strategy in Controller User Impact
Invalid Credentials FirebaseAuthException Caught by AsyncValue.guard() → state set to AsyncError Snackbar displays "Invalid email or password"
Banned Account DioException (USER_BANNED) Caught in isBannedError() utility helper Redirects to Banned Account Screen
FCM Device Registration Failed DioException / Timeout Swallowed inside try/catch block in _registerDevice() NON-FATAL: Login completes successfully anyway!

Special Section D: 4-Stage Transaction Analysis

Transaction 1: Primary Authentication
Authenticates identity with Firebase. Hard stop if invalid.
Transaction 2: Backend Database Sync
Synchronizes Firebase identity into PostgreSQL backend database. Mandatory for system integrity.
Transaction 3: Non-Fatal Push Device Registration
Registers device token. Isolated in a dedicated try/catch block so push notification issues never block user login.
Transaction 4: Downstream Profile Refresh
Forces currentUserProvider to re-fetch backend details so home screens display accurate user data immediately.

Special Section E: The Universal Riverpod Mutation Blueprint

Dart Universal Riverpod Mutation Blueprint
@riverpod
class FeatureController extends _$FeatureController {
  @override
  FutureOr<void> build() {} // 1. Initial State is idle void

  Future<void> submitMutation(InputData data) async {
    state = const AsyncValue.loading(); // 2. Trigger loading UI state
    
    state = await AsyncValue.guard(() async { // 3. Wrap async transaction
      final repo = ref.read(featureRepositoryProvider);
      await repo.performAction(data);
      if (!ref.mounted) return;

      // 4. Refresh dependent providers
      ref.invalidate(dependentDataProvider);
    });
  }
}

Special Section G: Controller vs Repository Responsibility Matrix

Responsibility Mutation Controller Repository ApiClient (Dio)
Owns AsyncLoading UI State YES NO NO
Orchestrates Transactions YES NO NO
Raw HTTP Endpoint Calls NO YES YES
Refreshes Downstream Providers YES NO NO
Contains Flutter / Riverpod Code YES NO NO

Special Section I: Step-by-Step Blueprint to Build Any Controller

Follow this exact 5-step recipe to construct controllers for Password Reset, Delete Account, or Image Uploads:

  1. Create an @riverpod class extending _$MyController with FutureOr<void> build() {}.
  2. Define an async public method (e.g. deleteAccount()).
  3. Set state = const AsyncValue.loading() at the start of the method.
  4. Wrap repository calls inside state = await AsyncValue.guard(() async { ... }).
  5. Add protective if (!ref.mounted) return; checks after every await boundary and refresh affected cached providers.

Special Section J: 15 Senior Flutter Interview Questions & Answers

Answer: Controllers manage state mutations for UI loading and error handling. Returning domain models directly tightly couples the controller to data models and makes state tracking difficult. Returning AsyncValue<void> allows UI widgets to bind loading spinners cleanly via state.isLoading.

Answer: If peripheral operations (push notifications) are not isolated, an FCM network failure would cause the entire login transaction to fail, preventing the user from entering the app even with valid credentials.

Answer: Inside event action methods (like login()), we execute one-time method calls on repositories. Using ref.watch() inside method bodies is invalid and would cause unnecessary controller re-creations.

📊 Senior Engineering Scorecard & Review

Single Responsibility
10 / 10: Dedicated strictly to action orchestration.
Transaction Isolation
9.5 / 10: Non-fatal FCM failures are safely isolated.
State Safety
10 / 10: Protective ref.mounted guards between all steps.
Scalability for 100 Developers
10 / 10: Universal pattern easily replicated for all CRUD actions.