← Назад к вопросам

Какие знаешь архитектурные паттерны на слое UI?

2.0 Middle🔥 111 комментариев
#State Management#Архитектура Flutter

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Архитектурные паттерны на слое UI в Flutter

На слое UI (Presentation) используются разные архитектурные паттерны для управления состоянием и организации кода. Каждый имеет свои плюсы и минусы.

1. MVC (Model-View-Controller)

MVC — классический паттерн, разделяющий приложение на три слоя.

// Model - бизнес логика и данные
class User {
  final String id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});
}

// Controller - управление логикой
class UserController {
  final UserRepository repository;
  late StreamController<User> _userStream;

  UserController(this.repository) {
    _userStream = StreamController<User>();
  }

  Future<void> loadUser(String id) async {
    try {
      final user = await repository.getUser(id);
      _userStream.add(user);
    } catch (e) {
      _userStream.addError(e);
    }
  }

  Stream<User> get userStream => _userStream.stream;
  void dispose() => _userStream.close();
}

// View - отображение
class UserView extends StatefulWidget {
  final UserController controller;

  const UserView({required this.controller});

  @override
  State<UserView> createState() => _UserViewState();
}

class _UserViewState extends State<UserView> {
  @override
  void initState() {
    super.initState();
    widget.controller.loadUser('123');
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User>(
      stream: widget.controller.userStream,
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return Text('User: ${snapshot.data!.name}');
        }
        return const CircularProgressIndicator();
      },
    );
  }
}

Плюсы: Простой для понимания, разделение ответственности Минусы: Controller может стать слишком большим, сложно тестировать

2. MVP (Model-View-Presenter)

MVP — улучшение MVC, Presenter полностью отделен от UI.

// Model
class Post {
  final String id;
  final String title;
  final String content;

  Post({required this.id, required this.title, required this.content});
}

// View Interface
abstract class PostView {
  void showLoading();
  void showPosts(List<Post> posts);
  void showError(String message);
}

// Presenter - чистая бизнес логика
class PostPresenter {
  final PostRepository repository;
  late PostView view;

  PostPresenter(this.repository);

  void loadPosts() {
    view.showLoading();
    repository.fetchPosts().then((posts) {
      view.showPosts(posts);
    }).catchError((error) {
      view.showError(error.toString());
    });
  }
}

// View Implementation
class PostsPage extends StatefulWidget implements PostView {
  late PostPresenter _presenter;

  PostsPage(PostRepository repository) {
    _presenter = PostPresenter(repository);
    _presenter.view = this;
  }

  @override
  State<PostsPage> createState() => _PostsPageState();

  @override
  void showLoading() => _PostsPageState()._showLoading();

  @override
  void showPosts(List<Post> posts) => _PostsPageState()._showPosts(posts);

  @override
  void showError(String message) => _PostsPageState()._showError(message);
}

class _PostsPageState extends State<PostsPage> {
  bool _isLoading = false;
  List<Post> _posts = [];
  String? _error;

  @override
  void initState() {
    super.initState();
    widget._presenter.loadPosts();
  }

  void _showLoading() => setState(() => _isLoading = true);
  void _showPosts(List<Post> posts) => setState(() {
    _isLoading = false;
    _posts = posts;
  });
  void _showError(String message) => setState(() {
    _isLoading = false;
    _error = message;
  });

  @override
  Widget build(BuildContext context) {
    if (_isLoading) return const CircularProgressIndicator();
    if (_error != null) return Text('Error: $_error');
    return ListView(
      children: _posts.map((p) => ListTile(title: Text(p.title))).toList(),
    );
  }
}

Плюсы: Presenter полностью тестируемый, чистая логика Минусы: Много boilerplate, View interface сложно поддерживать

3. MVVM (Model-View-ViewModel)

MVVM — ViewModel содержит UI state, View реактивно следит за изменениями.

// Model
class Comment {
  final String id;
  final String text;
  final String author;

  Comment({required this.id, required this.text, required this.author});
}

// ViewModel
class CommentsViewModel extends ChangeNotifier {
  final CommentRepository repository;
  bool _isLoading = false;
  List<Comment> _comments = [];
  String? _error;

  CommentsViewModel(this.repository);

  bool get isLoading => _isLoading;
  List<Comment> get comments => _comments;
  String? get error => _error;

  Future<void> loadComments(String postId) async {
    _isLoading = true;
    notifyListeners();

    try {
      _comments = await repository.getComments(postId);
      _error = null;
    } catch (e) {
      _error = e.toString();
      _comments = [];
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }

  Future<void> addComment(String postId, String text) async {
    try {
      final comment = await repository.addComment(postId, text);
      _comments.add(comment);
      notifyListeners();
    } catch (e) {
      _error = e.toString();
      notifyListeners();
    }
  }
}

// View
class CommentsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => CommentsViewModel(CommentsRepository()),
      child: _CommentsView(),
    );
  }
}

class _CommentsView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final viewModel = context.watch<CommentsViewModel>();

    if (viewModel.isLoading) {
      return const CircularProgressIndicator();
    }

    if (viewModel.error != null) {
      return Text('Error: ${viewModel.error}');
    }

    return ListView.builder(
      itemCount: viewModel.comments.length,
      itemBuilder: (context, index) {
        final comment = viewModel.comments[index];
        return ListTile(
          title: Text(comment.text),
          subtitle: Text(comment.author),
        );
      },
    );
  }
}

Плюсы: ViewModel полностью отделен, легко тестировать, реактивность Минусы: Нужен Provider/GetIt для инъекции зависимостей

4. BLoC (Business Logic Component)

BLoC — управление состоянием через events и states, лучший выбор для больших приложений.

// Events
abstract class TodoEvent extends Equatable {}

class LoadTodosEvent extends TodoEvent {
  @override
  List<Object?> get props => [];
}

class AddTodoEvent extends TodoEvent {
  final String title;
  AddTodoEvent(this.title);
  @override
  List<Object?> get props => [title];
}

class DeleteTodoEvent extends TodoEvent {
  final String id;
  DeleteTodoEvent(this.id);
  @override
  List<Object?> get props => [id];
}

// States
abstract class TodoState extends Equatable {}

class TodoInitial extends TodoState {
  @override
  List<Object?> get props => [];
}

class TodoLoading extends TodoState {
  @override
  List<Object?> get props => [];
}

class TodoLoaded extends TodoState {
  final List<Todo> todos;
  TodoLoaded(this.todos);
  @override
  List<Object?> get props => [todos];
}

class TodoError extends TodoState {
  final String message;
  TodoError(this.message);
  @override
  List<Object?> get props => [message];
}

// BLoC
class TodoBloc extends Bloc<TodoEvent, TodoState> {
  final TodoRepository repository;

  TodoBloc({required this.repository}) : super(TodoInitial()) {
    on<LoadTodosEvent>(_onLoadTodos);
    on<AddTodoEvent>(_onAddTodo);
    on<DeleteTodoEvent>(_onDeleteTodo);
  }

  Future<void> _onLoadTodos(
    LoadTodosEvent event,
    Emitter<TodoState> emit,
  ) async {
    emit(TodoLoading());
    try {
      final todos = await repository.fetchTodos();
      emit(TodoLoaded(todos));
    } catch (e) {
      emit(TodoError(e.toString()));
    }
  }

  Future<void> _onAddTodo(
    AddTodoEvent event,
    Emitter<TodoState> emit,
  ) async {
    if (state is TodoLoaded) {
      final todos = (state as TodoLoaded).todos;
      try {
        final newTodo = await repository.addTodo(event.title);
        emit(TodoLoaded([...todos, newTodo]));
      } catch (e) {
        emit(TodoError(e.toString()));
      }
    }
  }

  Future<void> _onDeleteTodo(
    DeleteTodoEvent event,
    Emitter<TodoState> emit,
  ) async {
    if (state is TodoLoaded) {
      final todos = (state as TodoLoaded).todos;
      try {
        await repository.deleteTodo(event.id);
        emit(TodoLoaded(todos.where((t) => t.id != event.id).toList()));
      } catch (e) {
        emit(TodoError(e.toString()));
      }
    }
  }
}

// View
class TodosPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => TodoBloc(repository: TodoRepository())..add(LoadTodosEvent()),
      child: Scaffold(
        appBar: AppBar(title: const Text('Todos')),
        body: BlocBuilder<TodoBloc, TodoState>(
          builder: (context, state) {
            if (state is TodoLoading) {
              return const Center(child: CircularProgressIndicator());
            }
            if (state is TodoLoaded) {
              return ListView.builder(
                itemCount: state.todos.length,
                itemBuilder: (context, index) {
                  final todo = state.todos[index];
                  return ListTile(
                    title: Text(todo.title),
                    trailing: IconButton(
                      icon: const Icon(Icons.delete),
                      onPressed: () => context.read<TodoBloc>().add(
                        DeleteTodoEvent(todo.id),
                      ),
                    ),
                  );
                },
              );
            }
            if (state is TodoError) {
              return Center(child: Text('Error: ${state.message}'));
            }
            return const Center(child: Text('Unknown state'));
          },
        ),
      ),
    );
  }
}

Плюсы: Полное разделение логики и UI, отличная тестируемость, масштабируемость, контроль потока Минусы: Много boilerplate, крутая кривая обучения

5. GetX (Simple State Management)

GetX — простая и мощная альтернатива BLoC.

// Controller
class CounterController extends GetxController {
  var count = 0.obs; // Observable

  void increment() {
    count++;
  }

  void decrement() {
    count--;
  }
}

// View
class CounterPage extends StatelessWidget {
  final controller = Get.put(CounterController());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: Obx(() => Text('Count: ${controller.count}')),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: controller.increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}

Плюсы: Минимальный boilerplate, простой API, быстрое прототипирование Минусы: Менее структурирован, может привести к плохим практикам в больших проектах

Сравнительная таблица

ПаттернСложностьТестируемостьМасштабируемостьКривая обучения
MVCСредняяХорошоСреднеНизкая
MVPВысокаяОтличноХорошоСредняя
MVVMСредняяХорошоХорошоСредняя
BLoCВысокаяОтличноОтличноВысокая
GetXНизкаяХорошоСреднеОчень низкая

Рекомендации

  • Маленькие приложения: GetX или MVVM
  • Средние приложения: BLoC или MVVM
  • Крупные приложения: BLoC (Clean Architecture)
  • Стартапы: GetX (быстро развивается)
  • Enterprise: BLoC + Clean Architecture

БОльшинство профессиональных Flutter приложений используют BLoC с Clean Architecture для максимальной масштабируемости и тестируемости.

Какие знаешь архитектурные паттерны на слое UI? | PrepBro