flutter
/

BLoC for Flutter Web: Responsive Apps & Web-Specific Patterns

Last Sync: Today

On this page

14
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

BLoC for Flutter Web: Responsive Apps & Web-Specific Patterns

Introduction

Flutter web brings your Flutter apps to the browser, but the web platform introduces unique challenges: responsive layouts, persistent storage, code splitting, and SEO considerations. BLoC works seamlessly on the web, but you need to adapt some patterns to leverage web capabilities. This guide covers everything you need to know to build robust, performant Flutter web applications using BLoC.

BLoC Works Everywhere

BLoC is platform‑agnostic. The same BLoC classes you write for mobile will work on web without changes. However, certain features (like local storage, responsive design, and routing) require web‑specific implementations. This guide focuses on those differences.

Setting Up Flutter Web with BLoC

Enable web support in your Flutter project:

BASHRead-only
1
flutter create --platforms=web my_app
cd my_app
flutter run -d chrome

Add flutter_bloc to pubspec.yaml. The rest of the BLoC setup is identical to mobile.

State Persistence on the Web

Unlike mobile, web browsers provide different storage mechanisms: localStorage (synchronous, up to ~5-10MB), IndexedDB (asynchronous, larger capacity), and sessionStorage. hydrated_bloc works on web out of the box using localStorage via shared_preferences or a custom adapter.

DARTRead-only
1
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // For web, hydrated_bloc will use localStorage automatically
  final storage = await HydratedStorage.build(
    storageDirectory: await getApplicationDocumentsDirectory(), // This works on web too
  );
  HydratedBloc.storage = storage;
  runApp(MyApp());
}

If you need IndexedDB for larger datasets, consider using hive with a custom backend or sqflite via sqflite_common_ffi (not recommended for web). For simple settings, shared_preferences (which uses localStorage on web) is sufficient.

Responsive UI with BLoC

Web apps need to adapt to different screen sizes. Use BLoC to manage layout state (e.g., breakpoints, sidebars). A common pattern is to listen to MediaQuery and update a BLoC state.

DARTRead-only
1
class LayoutBloc extends Bloc<LayoutEvent, LayoutState> {
  LayoutBloc() : super(LayoutState.initial()) {
    on<WindowResized>(_onWindowResized);
  }

  void _onWindowResized(WindowResized event, Emitter<LayoutState> emit) {
    final width = event.width;
    if (width >= 1200) {
      emit(state.copyWith(breakpoint: Breakpoint.desktop));
    } else if (width >= 800) {
      emit(state.copyWith(breakpoint: Breakpoint.tablet));
    } else {
      emit(state.copyWith(breakpoint: Breakpoint.mobile));
    }
  }
}

// In the UI
class ResponsiveWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LayoutBloc, LayoutState>(
      builder: (context, state) {
        switch (state.breakpoint) {
          case Breakpoint.mobile:
            return MobileLayout();
          case Breakpoint.tablet:
            return TabletLayout();
          case Breakpoint.desktop:
            return DesktopLayout();
        }
      },
    );
  }
}

Code Splitting & Lazy Loading

For large web apps, you can split your code into chunks that load only when needed. Use Dart's deferred imports. BLoC can be integrated with this pattern by lazy‑registering BLoCs in the route.

DARTRead-only
1
// In your route definition
case '/dashboard':
  return MaterialPageRoute(
    builder: (_) => DeferredWidget(
      futureFactory: () => loadDashboard(),
      childFactory: (module) => module.DashboardPage(),
    ),
  );

// dashboard.dart
library dashboard;

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

Future<void> loadDashboard() {
  // Ensure BLoC registrations happen only once
  return Future.value();
}

class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
  // ...
}

class DashboardPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => DashboardBloc(),
      child: DashboardView(),
    );
  }
}

Navigation and Routing

Flutter web supports URL‑based navigation. Use go_router or auto_route with BLoC. The BLoC pattern remains unchanged; the router simply triggers events or provides initial arguments to BLoCs via provider.

DARTRead-only
1
final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => HomePage(),
      routes: [
        GoRoute(
          path: 'details/:id',
          builder: (context, state) {
            final id = state.params['id']!;
            return BlocProvider(
              create: (_) => DetailsBloc()..add(LoadDetails(id)),
              child: DetailsPage(),
            );
          },
        ),
      ],
    ),
  ],
);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: router,
    );
  }
}

Handling Web-Specific Events

Web apps need to listen to scroll, resize, and visibility changes. You can use dart:html (carefully, with conditional imports) or Flutter's built‑in widgets. BLoC can react to these events.

DARTRead-only
1
class ScrollBloc extends Bloc<ScrollEvent, ScrollState> {
  ScrollBloc() : super(ScrollState.initial()) {
    on<ScrollUpdated>(_onScrollUpdated);
  }

  void _onScrollUpdated(ScrollUpdated event, Emitter<ScrollState> emit) {
    emit(state.copyWith(scrollY: event.scrollY));
  }
}

// In a widget
NotificationListener<ScrollNotification>(
  onNotification: (notification) {
    final scrollY = notification.metrics.pixels;
    context.read<ScrollBloc>().add(ScrollUpdated(scrollY));
    return false;
  },
  child: ListView(...),
);

SEO Considerations

Flutter web apps are client‑rendered, which can impact SEO. BLoC doesn't directly affect SEO, but you should ensure your app can be indexed by using server‑side rendering (if needed) or dynamic metadata. For SEO‑critical content, consider using flutter_html or rendering key content on the server. You can still manage SEO tags (e.g., <title> and <meta>) using the flutter_html or the flutter_inappwebview for dynamic updates.

Performance Optimizations

  • Use const constructors – Reduces widget rebuilds.
  • Lazy load BLoCs – Use BlocProvider with create instead of instantiating all BLoCs at startup.
  • Avoid heavy computations in event handlers – Offload to isolates or use compute.
  • Use buildWhen – Prevent unnecessary rebuilds on the web.
  • Minify and gzip – When deploying, use flutter build web --release and serve compressed assets.
  • Consider using CanvasKit or HTML renderer – Choose based on your performance needs (CanvasKit for consistent rendering, HTML for smaller bundle).

Deployment

Build your web app with:

BASHRead-only
1
flutter build web --release
# Output in build/web/

Serve the files with any static server. For advanced hosting, you can integrate with Firebase Hosting, Netlify, or Vercel.

Best Practices

  • Write platform‑agnostic BLoCs – Keep business logic separate from platform‑specific code.
  • Use conditional imports for web‑only code – Use dart:html only when necessary and guard with kIsWeb.
  • Test on multiple browsers – Chrome, Firefox, Safari, Edge.
  • Monitor performance with Lighthouse – Use DevTools Lighthouse tab to measure web vitals.
  • Handle offline support – Use service workers (via flutter_web_plugins) to cache assets, but BLoC can manage the state.

Common Mistakes

  • ❌ Using dart:io on web – Causes runtime errors. ✅ Use conditional imports or package: universal_io.
  • ❌ Storing large data in localStorage – Exceeds storage limits. ✅ Use IndexedDB via hive or sqflite_common_ffi.
  • ❌ Not testing on slow networks – Web apps may load slowly on 3G. ✅ Optimize assets and use code splitting.
  • ❌ Ignoring responsive design – App may look broken on desktop. ✅ Use BLoC to manage breakpoints.

Conclusion

BLoC is a powerful state management solution for Flutter web. By adapting your architecture to web‑specific concerns—responsive layouts, web storage, code splitting—you can build fast, maintainable web applications. Use the patterns and best practices in this guide to ensure your app runs smoothly across all browsers.

Try it yourself

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final storage = await HydratedStorage.build(
    storageDirectory: await getApplicationDocumentsDirectory(),
  );
  HydratedBloc.storage = storage;
  runApp(MyApp());
}

class CounterCubit extends HydratedCubit<int> {
  CounterCubit() : super(0);
  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);

  @override
  int fromJson(Map<String, dynamic> json) => json['value'] as int;

  @override
  Map<String, dynamic> toJson(int state) => {'value': state};
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BLoC Web Demo',
      home: BlocProvider(
        create: (_) => CounterCubit(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final cubit = context.watch<CounterCubit>();
    return Scaffold(
      appBar: AppBar(title: Text('BLoC on Web')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Counter:', style: TextStyle(fontSize: 20)),
            Text('${cubit.state}', style: TextStyle(fontSize: 40)),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: cubit.increment,
                  child: Icon(Icons.add),
                ),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: cubit.decrement,
                  child: Icon(Icons.remove),
                ),
              ],
            ),
            SizedBox(height: 20),
            Text('Refresh the page – counter persists!'),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

Which storage mechanism does hydrated_bloc use on the web by default?

A
IndexedDB
B
localStorage
C
sessionStorage
D
SQLite
Q2
of 3

What should you use to listen to window resize events in Flutter web?

A
dart:html
B
MediaQuery.of(context)
C
LayoutBuilder
D
Both B and C
Q3
of 3

How do you implement code splitting in Flutter web?

A
Using `deferred` imports
B
Using `flutter packages pub run build_runner build`
C
Using `FlutterWebPlugin`
D
It's automatic

Frequently Asked Questions

Can I use the same BLoC code for mobile and web?

Yes, BLoC is platform‑agnostic. However, you may need to conditionally use platform‑specific features (e.g., localStorage vs shared_preferences).

How do I store data persistently on the web?

Use hydrated_bloc which works with localStorage automatically. For larger data, consider using hive with a web adapter or IndexedDB.

Does Flutter web support `sqflite`?

Not directly. Use sqflite_common_ffi with dart:html or IndexedDB for web databases.

How do I implement URL‑based routing with BLoC?

Use go_router or auto_route. Pass route parameters to BLoCs via providers or events.

What renderer should I choose for my web app?

CanvasKit gives consistent rendering but larger bundle. HTML renderer has a smaller bundle but may have layout inconsistencies. Test both and choose based on your needs.

Previous

bloc devtools

Next

bloc firebase

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.