Skip to content
Blog

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

  1. Keep widgets stateless when possible — lift state into controllers
  2. Never import Data layer classes — only Domain entities and use cases
  3. Use freezed or built_value for immutable state classes
  4. Test controllers independently — no widget tree needed
  5. 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.