Можно ли использовать архитектуры MVVM и BLoC вместе в Flutter?

«Можно ли использовать архитектуры MVVM и BLoC вместе в Flutter?» — вопрос из категории Архитектура, который задают на 29% собеседований Flutter Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Да, эти паттерны отлично сочетаются, так как решают разные задачи. В моих проектах я часто комбинирую их:

  • BLoC управляет состоянием и бизнес-логикой
  • ViewModel (в контексте MVVM) выступает как адаптер между BLoC и View

Реализация на примере аутентификации:

// 1. BLoC для бизнес-логики
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final AuthRepository _repository;

  AuthBloc(this._repository) : super(AuthInitial()) {
    on<LoginRequested>((event, emit) async {
      emit(AuthLoading());
      try {
        final user = await _repository.login(event.email, event.password);
        emit(AuthAuthenticated(user));
      } catch (e) {
        emit(AuthError(e.toString()));
      }
    });
  }
}

// 2. ViewModel для подготовки данных для UI
class LoginViewModel {
  final AuthBloc _authBloc;

  LoginViewModel(this._authBloc);

  // Преобразуем State в удобные для View свойства
  bool get isLoading => _authBloc.state is AuthLoading;
  String? get error => _authBloc.state is AuthError 
      ? (_authBloc.state as AuthError).message 
      : null;
  bool get isAuthenticated => _authBloc.state is AuthAuthenticated;

  // Методы, которые View вызывает через ViewModel
  void login(String email, String password) {
    _authBloc.add(LoginRequested(email, password));
  }
}

// 3. View (Widget)
class LoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => AuthBloc(context.read<AuthRepository>()),
      child: BlocBuilder<AuthBloc, AuthState>(
        builder: (context, state) {
          final viewModel = LoginViewModel(context.read<AuthBloc>());

          return Scaffold(
            body: viewModel.isLoading
                ? CircularProgressIndicator()
                : Column(
                    children: [
                      if (viewModel.error != null)
                        Text(viewModel.error!, style: TextStyle(color: Colors.red)),
                      LoginForm(
                        onLogin: viewModel.login,
                      ),
                    ],
                  ),
          );
        },
      ),
    );
  }
}

Преимущества такого подхода:

  1. Четкое разделение ответственности:

    • BLoC: чистая бизнес-логика
    • ViewModel: подготовка данных для отображения
    • View: только отрисовка UI
  2. Тестируемость: можно тестировать BLoC и ViewModel отдельно

  3. Гибкость: легко заменить BLoC на другой state management (Provider, Riverpod)

  4. Поддержка сложных сценариев: ViewModel может агрегировать данные из нескольких BLoC

Когда это избыточно:

  • Для простых экранов (форма с 2 полями) используйте только StatefulWidget или Provider
  • В небольших приложениях BLoC или ViewModel по отдельности будет достаточно

В крупном e-commerce приложении мы использовали эту комбинацию: BLoC работал с API и кэшем, а ViewModel подготавливал данные для сложных списков товаров с фильтрацией и сортировкой.