Какие знаешь архитектурные паттерны на слое UI?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Архитектурные паттерны на слое 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 для максимальной масштабируемости и тестируемости.