The app was fine at three screens. It broke at twelve.
Widgets started calling APIs directly. Two screens fetched the same profile shape with different parsing. A UI tweak broke checkout.
The problem was not Flutter. Boundaries had not kept pace with the product.
Twelve screens, one widget tree
The app had grown into payments, maps, and account settings. Multiple vendors and internal services sat behind one growing tree of widgets.
We needed change without every feature rewrite dragging the rest.
Checkout broke on a UI tweak
Presentation owned orchestration, mapping, and error handling. Tests required pumping entire screens to check one rule.
Riverpod providers had become a second place to hide business logic.
Thin screens, clear repos
Keep screens thin. Repositories own data access. Use cases own rules that survive API churn.
DTOs stay at the edge. The domain does not mirror every field a vendor returns.
Repository boundary
class WalletRepository {
Future<WalletBalance> fetchBalance() async {
final dto = await _api.getWallet();
return WalletBalance.fromDto(dto);
}
}
// Screen only renders state from the providerOnce the seam was clear, we could test mapping without pumping widgets.
Add seams before the tree collapses
Add seams before the tree collapses. The goal is not purity. It is change without fear.