1. The Problem Statement: The Navigation Wait 🥶
Scenario: A user taps "Confirm Payment." Your backend successfully processes the transaction and saves the data to Firestore in milliseconds.
Observed Bug: A noticeable lag of 0.5 to 1.5 seconds occurs after the payment is confirmed but before the "Payment Success!" screen appears. The user feels the app is slow or stuck.
The Goal: The confirmation screen must appear instantly to reward the user and maintain a premium app feel.
2. Root Causes: The Double-Blockade 🚧
The lag is caused by two fundamental mistakes in how asynchronous tasks are handled, both of which unnecessarily block the main UI thread.
A. Blockade 1: The Serial await Trap (Unnecessary Waiting)
The Problem: Your code uses
awaiton every single background task, even if the navigation doesn't depend on it.Example of Bad Code:
Dart// After payment is successful: final receipt = await service.generateReceipt(); // Essential, must await await analytics.trackOrderConversion(); // ❌ Don't need to await this! await user.updateTotalOrders(); // ❌ Don't need to await this! // Navigation can only happen after ALL three are complete.The Effect: You serialise tasks that could run concurrently, forcing the user to wait for non-essential background processes to finish.
B. Blockade 2: The Synchronous notifyListeners() Block (The Final Freeze)
The Problem: You call notifyListeners() (or a similar state change) right before navigating.
The Effect: The
notifyListeners()call executes synchronously on the UI thread, forcing a complete rebuild of every widget listening to that state (e.g., the complex "Account Dashboard" or "Order History" screen). This massive rebuild must finish before thereturnstatement can run, leading to the final, observable freeze just before navigation.
3. Design Best Practices: Decouple All Non-Essential Work 🚀
To eliminate all lag, you must apply two principles:
1. Never block for non-essential data and
2. Defer heavy UI work.
Step 1: Remove Unnecessary Blocking
Allow background tasks to run concurrently without await.
// Inside your ViewModel (after the primary payment write completes)
// Essential task (must await)
final receipt = await service.generateReceipt();
// Non-essential tasks (run in the background without blocking)
analytics.trackOrderConversion();
user.updateTotalOrders();
Step 2: Decouple the UI Rebuild with Future.microtask
This is the critical fix to solve the final lag caused by the synchronous rebuild.
| Action in the ViewModel | Rationale | Code Principle |
| Local Update | Update local variables instantly (fast). | totalOrders += 1; |
| The CRITICAL Fix | Defer the heavy rebuild. Pushes the expensive synchronous rebuild to the next available event cycle. | Future.microtask(() => notifyListeners()); |
| Return | Release the UI thread. The function returns, allowing navigation to execute immediately. | return receipt; |
Final Code Strategy (Putting it all together):
// Inside your ViewModel.confirmPayment
final receipt = await service.generateReceipt(); // Await essential task
// 1. Synchronously update local state
totalOrders += 1;
orders.insert(0, receipt);
// 2. 🌟 CRITICAL FIX: Defer heavy rebuild 🌟
Future.microtask(() => notifyListeners());
// 3. Trigger background refreshes (no await)
analytics.trackOrderConversion();
user.updateTotalOrders();
// 4. Return immediately (Enables instant navigation)
return receipt;
By applying these two fixes, you ensure the user is instantly celebrated on the success screen, and all data updates and rebuilds are handled efficiently in the background.
No comments:
Post a Comment