Mutation Controller Architecture (login_controller.dart)
🎯 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)
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
AsyncLoading(). UI disables input fields and displays loading indicator.repo.signInWithEmail(email, password). Authenticates credentials with Firebase.repo.syncUser() (`POST /users/sync`). Ensures user record exists in PostgreSQL backend database.registerDeviceWithFcm(). Exceptions are caught and ignored as non-fatal.ref.read(currentUserProvider.notifier).refresh() to fetch fresh user profile data.Special Section B: Visual Orchestration Architecture
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
try/catch block so push notification issues never block user login.Special Section E: The 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:
- Create an
@riverpodclass extending_$MyControllerwithFutureOr<void> build() {}. - Define an async public method (e.g.
deleteAccount()). - Set
state = const AsyncValue.loading()at the start of the method. - Wrap repository calls inside
state = await AsyncValue.guard(() async { ... }). - 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
ref.mounted guards between all steps.