Ответ
Да, эти паттерны отлично сочетаются, так как решают разные задачи. В моих проектах я часто комбинирую их:
- 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,
),
],
),
);
},
),
);
}
}
Преимущества такого подхода:
-
Четкое разделение ответственности:
- BLoC: чистая бизнес-логика
- ViewModel: подготовка данных для отображения
- View: только отрисовка UI
-
Тестируемость: можно тестировать BLoC и ViewModel отдельно
-
Гибкость: легко заменить BLoC на другой state management (Provider, Riverpod)
-
Поддержка сложных сценариев: 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 не лезет в бизнес-логику, он только готовит данные для отображения. А Виджет вообще превращается в тупую мартышку, которая только кнопки жмёт и текст выводит. Идеальная схема, хуй с горы, не придерешься.
Плюсов — овердохуища:
- Всё по полочкам. Каждый занимается своим делом, никто никому не мешает. Тестировать — одно удовольствие, можно BLoC отдельно от ViewModel гонять.
- Гибкость дикая. Захотел сменить state management — да похуй, меняй только ViewModel, а BLoC пусть себе живёт.
- Для сложных штук — то, что надо. У нас в одном проекте ViewModel собирал данные аж из трёх разных BLoC'ов для одного экрана с фильтрами, и всё летало.
Но, конечно, не надо с этим фанатеть. Если у тебя экран — одна кнопка и надпись «Hello World», то это пиздопроебибна, прости господи. StatefulWidget'а за глаза хватит, не городи огород.
А вот в большом проекте, особенно где много данных и сложная логика отображения — это прям спасение. У нас так на одном маркетплейсе сделали: BLoC тащит товары с бэка, а ViewModel их фильтрует, сортирует и готовит для списка. Красота, ядрёна вошь!