Можно ли использовать архитектуры MVVM и BLoC вместе в 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 подготавливал данные для сложных списков товаров с фильтрацией и сортировкой.

Ответ 18+ 🔞

Э, слушай, а ведь это реально годная связка, как водка с пивом — только без похмелья. Ты вон смотри, как они друг друга дополняют, просто красота, ебать мои старые костыли.

BLoC — это типа наш главный по мозгам и состоянию. Он там с репозиториями общается, запросы шлёт, ошибки ловит. А ViewModel — это такой хитрая жопа-переводчик. Берёт сырые данные от BLoC'а и делает из них конфетку, которую уже можно без боли в глазах запихнуть в UI.

Вот смотри, как на примере логина это выглядит, реально как по учебнику:

// 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 — переводчик с языка BLoC на человеческий
class LoginViewModel {
  final AuthBloc _authBloc;

  LoginViewModel(this._authBloc);

  // Вот тут магия: превращаем State в простые булевы флаги
  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;

  // А это просто проброс вызова дальше, в BLoC
  void login(String email, String password) {
    _authBloc.add(LoginRequested(email, password));
  }
}

// 3. Виджет — тупая рожа, которая только рисует
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,
                      ),
                    ],
                  ),
          );
        },
      ),
    );
  }
}

И знаешь, в чём прикол? Всё становится на свои места, как будто пазл сложился. BLoC не парится, как данные показать, он просто их готовит. ViewModel не лезет в бизнес-логику, он только готовит данные для отображения. А Виджет вообще превращается в тупую мартышку, которая только кнопки жмёт и текст выводит. Идеальная схема, хуй с горы, не придерешься.

Плюсов — овердохуища:

  1. Всё по полочкам. Каждый занимается своим делом, никто никому не мешает. Тестировать — одно удовольствие, можно BLoC отдельно от ViewModel гонять.
  2. Гибкость дикая. Захотел сменить state management — да похуй, меняй только ViewModel, а BLoC пусть себе живёт.
  3. Для сложных штук — то, что надо. У нас в одном проекте ViewModel собирал данные аж из трёх разных BLoC'ов для одного экрана с фильтрами, и всё летало.

Но, конечно, не надо с этим фанатеть. Если у тебя экран — одна кнопка и надпись «Hello World», то это пиздопроебибна, прости господи. StatefulWidget'а за глаза хватит, не городи огород.

А вот в большом проекте, особенно где много данных и сложная логика отображения — это прям спасение. У нас так на одном маркетплейсе сделали: BLoC тащит товары с бэка, а ViewModel их фильтрует, сортирует и готовит для списка. Красота, ядрёна вошь!