Flutter Clean Architecture: The Presentation Layer
Master the Presentation layer in Flutter Clean Architecture — how widgets and controllers work together to build reactive, testable UIs.
Published on • September 17, 2026
AI Assistant

The Presentation Layer’s Role
The Presentation layer is the outermost ring of Clean Architecture. It owns everything the user sees and interacts with: Widgets (the view) and Controllers (the state management and business logic bridge).
This layer is responsible for:
- Rendering UI based on state
- Handling user input and gestures
- Delegating business logic to Domain use cases
- Managing navigation and lifecycle
Widgets: The View
Widgets should be dumb. They receive state and emit events — nothing more. Avoid putting business logic inside widget classes.
class LoginScreen extends StatelessWidget {
final LoginState state;
final VoidCallback onLogin;
final ValueChanged<String> onChanged;
const LoginScreen({
required this.state,
required this.onLogin,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
TextField(onChanged: onChanged),
if (state.isLoading) CircularProgressIndicator(),
ElevatedButton(
onPressed: state.isLoading ? null : onLogin,
child: Text('Sign In'),
),
],
),
);
}
}
Controllers: The State Manager
Controllers sit between Widgets and Use Cases. They hold UI state, call use cases, and expose reactive streams (via Riverpod, BLoC, or ChangeNotifier).
class LoginController extends ChangeNotifier {
final SignInUseCase _signIn;
LoginState _state = LoginState();
LoginState get state => _state;
LoginController(this._signIn);
void onEmailChanged(String email) {
_state = _state.copyWith(email: email);
notifyListeners();
}
Future<void> login() async {
_state = _state.copyWith(isLoading: true);
notifyListeners();
try {
await _signIn(_state.email, _state.password);
_state = _state.copyWith(isLoading: false, isSuccess: true);
} catch (e) {
_state = _state.copyWith(isLoading: false, error: e.toString());
}
notifyListeners();
}
}
Best Practices
- Keep widgets stateless when possible — lift state into controllers
- Never import Data layer classes — only Domain entities and use cases
- Use freezed or built_value for immutable state classes
- Test controllers independently — no widget tree needed
- Separate screen and widget directories — screens compose widgets
Conclusion
A well-structured Presentation layer makes your Flutter app a joy to test, refactor, and extend. Keep widgets dumb, controllers focused, and watch your UI complexity become manageable.