Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
149543 stories
·
33 followers

Level up your data security at Microsoft 365 Community Conference

1 Share

Technology is evolving at a startling rate, and it’s no longer enough for an organization to maintain their security measures, they must evolve along with it. Don’t let your data, employees, or clients become another headline. Join us in Orlando on April 21–23 for the Microsoft 365 Community Conference, where you’ll learn how to safeguard your most valuable data during expert-led discussions and hands-on sessions

.

Chart your course forward with Microsoft’s security roadmap

Do you ever wish you had a crystal ball that could show you how the changing tech landscape is going to affect your data protection needs? We can give you the next-best thing, an inside look at where the future of work is taking security solutions and what that means for you, straight from the experts leading the way.

Register now, and you’ll be in the room as Vasu Jakkal, Corporate Vice President, Microsoft Security and Rohan Kumar, Corporate Vice President, Microsoft Security unveil Microsoft’s vision for securing the new frontier of AI. They’ll discuss the ways frontier firms are protecting their data, identities, and models amid rapid AI adoption and Agents. Microsoft is helping IT and security teams adapt to AI driven work with security built in, not bolted on, gain practical guidance on how organizations are modernizing security controls, reducing data exposure, and supporting AI adoption at scale, without adding complexity for admins or end users.

And that’s not all; we have a full schedule of innovators and trailblazers scheduled throughout the program. You’ll discover exclusive insights, best practices, and strategies that will redefine your organization’s data security. Check out the speaker directory on the Microsoft 365 Community Conference homepage and then register to save your seat today.

Attend a session for every security need

We all know that securing our data is important, but it can be hard to know where to start when different sectors require different security measures. That’s why we’ve designed the sessions at Microsoft 365 Community Conference to provide attendees with both a comprehensive overview and a deep dive into specific critical topics.

• Agent 365: The control plane for all Agents with Nikita Bandyopadhyay and Sesha Mani

• Agent Lifecycle Management and Governance leveraging FastTrack with Azharullah Meer and Pratik Bhusal

• Copilot readiness & resiliency with M365 Backup & Archive with Sree Lakshmi and Aditi Gangwar

• Copilot Studio Governance for Public Sector with Richie Wallace and William McLendon

• Deep dive into Agent insights and content governance across SharePoint and Microsoft 365 with Nikita Bandyopadhyay

• Extending Microsoft Purview with APIs & SDK: Governance-by-Design for AI Apps and Agents with Martin Gagne

• How Microsoft Digital adopted Baseline Security Mode to improve Microsoft's security posture with Adriana Wood

• How Microsoft Does IT: Managing and governing Agents - empower with risk aligned oversight with David Johnson, Naveen Jangir, and Mike Powers

• How Microsoft Does IT: Microsoft 365 Governance in the age of Copilot & agents with David Johnson

• Microsoft Baseline Security Mode: Simplify, secure, succeed with Adriana Wood and Sesha Mani

• Microsoft Purview: AI-Powered Data Security for Microsoft 365 with Aashish Ramdas

• Mission Readiness - Cybersecurity and Copilot in the Public Sector with Karuana Gatimu

• Protect and govern agents with Microsoft Purview with Shilpa Ranganathan

• Protect your data from oversharing to AI with Microsoft Purview with Roberto Yglesias

• Public Sector Roadmap Review with Karuana Gatimu and Tuwanda Perez

• Securing AI with Microsoft Purview: From Visibility to Action with Shilpa Ranganathan

• Tracing the Truth: Accelerate Insider Risk Investigations with Microsoft Purview with Rod Trent

• What's new in Security & Compliance for SharePoint, OneDrive, and Teams with Sanjoyan Mustafi, Vithalprasad Gaitonde

No matter your security needs, we have a can’t-miss session waiting for you at Microsoft 365 Community Conference. Explore all Security, Compliance, and Governance sessions here.

 

You can’t halt the march of progress, but by attending the Microsoft 365 Community Conference, you can make sure you're in a position to take advantage of it. Register now, and we’ll see you April 21–23 in Orlando.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

App Hosting has in-console environment variables and faster builds

1 Share
Manage environment variables in the Firebase console, and learn about a small change our engineering team made to improve build times for everyone.
Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

Flutter BLoC Best Practices You're Probably Missing

1 Share

Cover

Reading source code is honestly one of the best ways to understand how things actually work. I've been building Flutter apps with the BLoC pattern for years, and I thought I had it figured out, until I opened the bloc repository and started digging through the code.

What I found was code that makes you go "oh, that's why." That StateError that crashes your app at 2 AM? It's literally one if statement. The "my UI isn't updating" bug that haunts every developer? One line of code explains the whole thing.

I realized that understanding these patterns at the source level is exactly what separates solid state management from fragile code. So I decided to walk you through the BLoC source code and show how BLoC lint rules connect directly to what's actually happening under the hood.

info

You can find the full source code examples for all the rules discussed in this article in our examples repository.

Let's get started!

The Foundation

Every Bloc and Cubit extends BlocBase, which implements the core lifecycle. Let's look at the emit() method, it's where all the magic (and bugs) happen:

From bloc/lib/src/bloc_base.dart
abstract class BlocBase<State>
implements StateStreamableSource<State>, Emittable<State>, ErrorSink {

BlocBase(this._state) {
_blocObserver.onCreate(this);
}

final _stateController = StreamController<State>.broadcast();
State _state;
bool _emitted = false;


State get state => _state;


Stream<State> get stream => _stateController.stream;


bool get isClosed => _stateController.isClosed; // 👈 Delegates to stream controller




void emit(State state) {
try {
if (isClosed) {
throw StateError('Cannot emit new states after calling close');
}
if (state == _state && _emitted) return; // 👈 Same-instance check!
onChange(Change<State>(currentState: this.state, nextState: state));
_state = state;
_stateController.add(_state);
_emitted = true;
} catch (error, stackTrace) {
onError(error, stackTrace);
rethrow;
}
}



Future<void> close() async {
_blocObserver.onClose(this);
await _stateController.close(); // 👈 Closing the controller marks isClosed as true
}
}

Two lines in this code explain two of the most common BLoC bugs. Let me show you.

The isClosed Guard

if (isClosed) {
throw StateError('Cannot emit new states after calling close');
}

This is the source of "Cannot emit new states after calling close", one of the most common BLoC errors.

Here's the scenario: user opens a screen, triggers an API call, then navigates away immediately. The widget disposes, close() gets called, but the async operation is still running. When it finishes and tries to emit... crash.

This is exactly what check-is-not-closed-after-async-gap catches.

❌ Bad: Will crash if BLoC closes during the await
class SearchBlocBad extends Bloc<SearchEvent, SearchState> {
SearchBlocBad(this._repository) : super(SearchInitialImpl()) {
on<SearchQueryChanged>(_onQueryChanged);
}

final SearchRepository _repository;

Future<void> _onQueryChanged(
SearchQueryChanged event,
Emitter<SearchState> emit,
) async {
emit(SearchLoadingState());
final results = await _repository.search(event.query);
// 💥 CRASH: If user navigated away during the search, this emit throws
emit(SearchSuccessImpl(results));
}
}

The fix is simple, just check isClosed before emitting:

✅ Good: Always check isClosed after async gaps
  Future<void> _onQueryChanged(
SearchQueryChanged event,
Emitter<SearchState> emit,
) async {
emit(SearchLoadingState());
final results = await _repository.search(event.query);
// ✅ Safe: Check before emitting
if (!isClosed) {
emit(SearchSuccessImpl(results));
}
}

The rule also supports custom event, listening methods through configuration:

analysis_options.yaml
dcm:
rules:
- check-is-not-closed-after-async-gap:
additional-methods:
- customOn

This single rule eliminates an entire category of production crashes. I can't stress enough how important it is.

The Equality Short-Circuit

Now look at the second critical line:

if (state == _state && _emitted) return;

This line silently skips emits when the state hasn't changed. The == operator uses your state's equality implementation. If you emit the same instance, identical(this, other) returns true and the UI never rebuilds.

This is the infamous "my UI isn't updating" bug, and it's what emit-new-bloc-state-instances catches.

❌ Bad: Emitting the same instance, UI won't update
class UserBloc extends Bloc<UserEvent, UserState> {
UserBloc() : super(UserState()) {
on<UpdateNameEvent>((event, emit) {
// 💥 This modifies the existing state object
state.name = event.name;

// 💥 Same instance, same reference, UI ignores this emit!
emit(state);
});
}
}

The solution is to always create a new state instance:

✅ Good: Always emit a new instance
class UserBloc extends Bloc<UserEvent, UserState> {
UserBloc() : super(const UserState()) {
on<UpdateNameEvent>((event, emit) {
// ✅ New instance with updated values
emit(state.copyWith(name: event.name));
});
}
}


class UserState {
final String name;

const UserState({this.name = ''});

UserState copyWith({String? name}) {
return UserState(name: name ?? this.name);
}
}

This bug is a nightmare to debug because everything looks right. The event fires, the handler runs, emit gets called... but nothing happens on screen. The rule catches it instantly.

Read more about BLoC Library FAQ: State not updating

The Provider Lifecycle

The BlocProvider source code shows exactly why two common memory bugs happen:

From flutter_bloc/lib/src/bloc_provider.dart
class BlocProvider<T extends StateStreamableSource<Object?>>
extends SingleChildStatelessWidget {

// Constructor for create: mode
const BlocProvider({
required T Function(BuildContext context) create,
Key? key,
this.child,
this.lazy = true,
}) : _create = create,
_value = null,
super(key: key, child: child);

// Named constructor for .value mode
const BlocProvider.value({
required T value,
Key? key,
this.child,
}) : _value = value,
_create = null,
lazy = true,
super(key: key, child: child);

final Widget? child;
final bool lazy;
final T Function(BuildContext context)? _create;
final T? _value;


Widget buildWithChild(BuildContext context, Widget? child) {
assert(
child != null,
'$runtimeType used outside of MultiBlocProvider must specify a child',
);
final value = _value;
return value != null
? InheritedProvider<T>.value(
value: value,
startListening: _startListening,
lazy: lazy,
// ❌ NO dispose callback for .value!
child: child,
)
: InheritedProvider<T>(
create: _create,
dispose: (_, bloc) => bloc.close(), // ✅ Auto-close for create:
startListening: _startListening,
lazy: lazy,
child: child,
);
}
}

Look at the two code paths:

  • create: mode: Passes dispose: (_, bloc) => bloc.close() to the InheritedProvider
  • .value mode: No dispose callback at all

This is exactly why the provider lifecycle rules exist.

BLoC Provider LifecycleBLoC Provider Lifecycle

Using the Wrong Provider Type

Before we even get to create: vs .value, there's an even more fundamental mistake:

| Using a generic Provider when you should be using BlocProvider.

If your project uses both bloc and provider packages, it's easy to accidentally wrap a BLoC in a plain Provider, which bypasses the lifecycle management that BlocProvider provides.

This is what prefer-correct-bloc-provider catches.

❌ Bad: Using Provider instead of BlocProvider
// 💥 This won't auto-close the BLoC when disposed!
final provider = Provider<CounterBloc>(
create: (context) => CounterBloc(),
child: const CounterPage(),
);
✅ Good: Use BlocProvider for BLoCs
final blocProvider = BlocProvider<CounterBloc>(
create: (context) => CounterBloc(),
child: const CounterPage(),
);

Passing Existing Instances to create:

BlocProvider(create: ...) takes ownership of the BLoC instance. It will call close() when the provider is disposed. But if you pass an existing instance to create:, you've created a lifecycle mismatch.

This is what avoid-existing-instances-in-bloc-provider catches.

❌ Bad: BlocProvider will close an instance it doesn't own
class MyApp extends StatelessWidget {
// 🚨 This BLoC is created outside the provider
final counterBloc = CounterBloc();


Widget build(BuildContext context) {
return BlocProvider(
// 💥 BlocProvider will close this when disposed,
// but you might try to use it elsewhere!
create: (_) => counterBloc,
child: const CounterPage(),
);
}
}

Instead, let the provider create and own the instance:

✅ Good: Let BlocProvider create and own the instance
class MyApp extends StatelessWidget {

Widget build(BuildContext context) {
return BlocProvider(
// ✅ Provider creates it, provider owns it, provider closes it
create: (_) => CounterBloc(),
child: const CounterPage(),
);
}
}

This causes double-close errors or worse use-after-close bugs that are super hard to track down. The BLoC seems fine sometimes and crashes randomly other times.

Instantiating in .value

This is the flip side. When you use .value, you're telling the provider: "I'm giving you an existing instance; don't touch its lifecycle." But if you instantiate a new BLoC directly in .value, nobody will close it.

This is what avoid-instantiating-in-bloc-value-provider catches.

❌ Bad: Memory leak, this BLoC will never be closed
BlocProvider.value(
// 💥 MEMORY LEAK: Who closes this? Nobody!
value: CounterBloc(),
child: const CounterPage(),
);

When using .value, you must manage the lifecycle yourself:

✅ Good: Pass an existing instance that you manage
class ParentWidget extends StatefulWidget {

State<ParentWidget> createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
late final CounterBloc _counterBloc;


void initState() {
super.initState();
_counterBloc = CounterBloc();
}


void dispose() {
_counterBloc.close(); // ✅ You manage the lifecycle
super.dispose();
}


Widget build(BuildContext context) {
return BlocProvider.value(
value: _counterBloc, // ✅ Existing instance
child: const CounterPage(),
);
}
}

Silent memory leaks accumulate until your app becomes sluggish and eventually crashes. These leaks don't show up in testing but destroy user experience in production.

info

We also have written a comprehensive blog post about memory leaks and how it happens in Flutter in general. If you are interested to deep dive, read Let's talk about memory leaks in Dart and Flutter.

The Event System

The Bloc class builds on BlocBase by adding event-driven architecture. Let's look at how on<E>() works:

From bloc/lib/src/bloc.dart
abstract class Bloc<Event, State> extends BlocBase<State>
implements BlocEventSink<Event> {

Bloc(super.initialState);

final _eventController = StreamController<Event>.broadcast();
final _subscriptions = <StreamSubscription<dynamic>>[];
final _handlers = <_Handler>[];
final _emitters = <_Emitter<dynamic>>[];


void add(Event event) {
assert(() {
final handlerExists = _handlers.any((handler) => handler.isType(event));
if (!handlerExists) {
final eventType = event.runtimeType;
throw StateError(
'''add($eventType) was called without a registered event handler.\n'''
'''Make sure to register a handler via on<$eventType>((event, emit) {...})''',
);
}
return true;
}());

try {
onEvent(event);
_eventController.add(event);
} catch (error, stackTrace) {
onError(error, stackTrace);
rethrow;
}
}

void on<E extends Event>(
EventHandler<E, State> handler, {
EventTransformer<E>? transformer,
}) {
// Prevent duplicate handlers for the same event type
assert(() {
final handlerExists = _handlers.any((handler) => handler.type == E);
if (handlerExists) {
throw StateError(
'on<$E> was called multiple times. '
'There should only be a single event handler per event type.',
);
}
_handlers.add(_Handler(isType: (dynamic e) => e is E, type: E));
return true;
}());

final subscription = (transformer ?? _eventTransformer)(
_eventController.stream.where((event) => event is E).cast<E>(),
(dynamic event) {
void onEmit(State state) {
if (isClosed) return;
if (this.state == state && _emitted) return;
onTransition(Transition(
currentState: this.state,
event: event as E,
nextState: state,
));
emit(state);
}
// ... handler invocation with Emitter
},
).listen(null);

_subscriptions.add(subscription);
}
}

Key takeaways from this code:

  • Events flow through a StreamController: The add() method doesn't directly call handlers, it pushes to a stream that handlers subscribe to. This is the unidirectional flow that public methods violate.

  • onTransition() captures the full picture: Unlike onChange() (which only has before/after state), transitions include the triggering event. This is why BlocObserver can provide complete tracing if you use events.

  • Guard clauses everywhere: Notice if (isClosed) return; appears multiple times. The library tries to protect you, but async gaps can still slip through.

The Change and Transition Types

Understanding these immutable records explains why we need immutable events and states:

From bloc/lib/src/change.dart and transition.dart

class Change<State> {
const Change({required this.currentState, required this.nextState});

final State currentState;
final State nextState;


bool operator ==(Object other) =>
identical(this, other) ||
other is Change<State> &&
runtimeType == other.runtimeType &&
currentState == other.currentState &&
nextState == other.nextState;
}


class Transition<Event, State> extends Change<State> {
const Transition({
required State currentState,
required this.event,
required State nextState,
}) : super(currentState: currentState, nextState: nextState);

final Event event; // 👈 The event is captured in the transition
}

If your events are mutable, a Transition captured by BlocObserver could have its event changed after being logged, creating weird bugs that are almost impossible to track down.

Public Methods Bypass Events

The entire point of the BLoC pattern is unidirectional data flow. Events go in, states come out. When you add public methods to a BLoC, you create a back door that bypasses the event system.

This is what avoid-bloc-public-methods catches.

❌ Bad: Public methods bypass the event system
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);

// 🚨 This bypasses events entirely!
// BlocObserver won't see this change.
// You can't replay this action.
// State history becomes incomplete.
void incrementDirectly() {
emit(state + 1);
}


void onChange(Change<int> change) {
super.onChange(change);
print(change); // Won't log incrementDirectly() calls properly
}
}

Keep all state mutations flowing through events:

✅ Good: Everything flows through events
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<IncrementPressed>((event, emit) => emit(state + 1));
}

// Keep implementation details private
void _logAnalytics() { /* ... */ }


void onChange(Change<int> change) {
super.onChange(change);
print(change); // ✅ All state changes are tracked
}
}

// Usage in widget:
context.read<CounterBloc>().add(IncrementPressed());

When you use events, Bloc.on<E>() creates Transition objects that include the event. But Cubit.emit() (and direct emit() calls from public methods) only create Change objects, no event information. This is why BlocObserver.onTransition() gives you full traceability, but only if you actually use events.

Every public method is technical debt. Your future self (or teammates) will have to figure out which state changes come from events and which come from direct method calls.

Context and BLoC-to-BLoC

Two more architectural rules deserve attention.

BuildContext Doesn't Belong in BLoCs

When you pass BuildContext to a BLoC event or Cubit method, you're creating a vulnerable situation. The context is tied to a specific widget in the tree. If that widget is disposed while the BLoC is still processing, you get errors like "looking up a deactivated widget's ancestor."

Beyond crashes, passing context destroys testability. How do you unit test a BLoC that needs a real BuildContext?

This is what avoid-passing-build-context-to-blocs catches.

❌ Bad: Context coupling creates crashes and untestable code
// In your widget:
bloc.add(LoadUserEvent(context)); // 🚨 Passing context to event

// In your event:
class LoadUserEvent extends UserEvent {
final BuildContext context; // 💥 This will cause problems
LoadUserEvent(this.context);
}

// In your Cubit:
class SettingsCubit extends Cubit<SettingsState> {
// 💥 What happens when this context is no longer mounted?
void loadTheme(BuildContext context) async {
final theme = Theme.of(context);
// ... use theme
}
}

Extract the data you need before passing it to the BLoC:

✅ Good: Pass data, not context
// In your widget:
final userId = context.read<AuthBloc>().state.userId;
bloc.add(LoadUserEvent(userId)); // ✅ Pass the data you need

// In your event:
class LoadUserEvent extends UserEvent {
final String userId; // ✅ Just the data
LoadUserEvent(this.userId);
}

// In your Cubit:
class SettingsCubit extends Cubit<SettingsState> {
final ThemeRepository _themeRepository;

SettingsCubit(this._themeRepository) : super(SettingsInitial());

void loadTheme() async {
// ✅ Get theme data from repository, not context
final theme = await _themeRepository.getCurrentTheme();
emit(SettingsLoaded(theme));
}
}

BLoCs Should Not Know About Each Other

It's tempting to inject one BLoC into another when they need to coordinate. But this creates tight coupling and circular dependency risks. If BlocA depends on BlocB, and BlocB needs data from BlocA, you've got a maintenance nightmare.

This is what avoid-passing-bloc-to-bloc catches.

❌ Bad: Direct BLoC-to-BLoC coupling
class CartBloc extends Bloc<CartEvent, CartState> {
final AuthBloc authBloc; // 🚨 Direct dependency
late final StreamSubscription _authSubscription;

CartBloc(this.authBloc) : super(CartInitial()) {
// 🚨 Tight coupling: CartBloc knows too much about AuthBloc
_authSubscription = authBloc.stream.listen((authState) {
if (authState is Unauthenticated) {
add(ClearCartEvent());
}
});
}


Future<void> close() {
_authSubscription.cancel();
return super.close();
}
}

There are two better approaches: coordinate in the widget layer, or use a shared stream:

✅ Good: Coordinate at the presentation layer or through repositories
// Option 1: Coordinate in the widget layer
class CartPage extends StatelessWidget {

Widget build(BuildContext context) {
return BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is Unauthenticated) {
// ✅ Widget coordinates between BLoCs
context.read<CartBloc>().add(ClearCartEvent());
}
},
child: const CartView(),
);
}
}

// Option 2: Use a shared repository/stream
class CartBloc extends Bloc<CartEvent, CartState> {
final CartRepository _cartRepository;
final Stream<AuthStatus> _authStatusStream; // ✅ Stream, not BLoC

CartBloc({
required CartRepository cartRepository,
required Stream<AuthStatus> authStatusStream,
}) : _cartRepository = cartRepository,
_authStatusStream = authStatusStream,
super(CartInitial()) {
_authStatusStream.listen((status) {
if (status == AuthStatus.unauthenticated) {
add(ClearCartEvent());
}
});
}
}
BLoC Coordination PatternsBLoC Coordination Patterns

The "quick fix" of passing one BLoC to another always leads to maintenance headaches as the app grows.

The BlocBuilder Rebuild Logic

Let's look at how BlocBuilder decides when to rebuild:

From flutter_bloc/lib/src/bloc_builder.dart
typedef BlocBuilderCondition<S> = bool Function(S previous, S current);

class BlocBuilder<B extends StateStreamable<S>, S>
extends BlocBuilderBase<B, S> {

const BlocBuilder({
required this.builder,
Key? key,
B? bloc,
BlocBuilderCondition<S>? buildWhen,
}) : super(key: key, bloc: bloc, buildWhen: buildWhen);

final BlocWidgetBuilder<S> builder;


Widget build(BuildContext context, S state) => builder(context, state);
}

class _BlocBuilderBaseState<B extends StateStreamable<S>, S>
extends State<BlocBuilderBase<B, S>> {

late B _bloc;
late S _state;


void initState() {
super.initState();
_bloc = widget.bloc ?? context.read<B>();
_state = _bloc.state;
}


Widget build(BuildContext context) {
if (widget.bloc == null) {
// Trigger a rebuild if the bloc reference has changed.
context.select<B, bool>((bloc) => identical(_bloc, bloc));
}
return BlocListener<B, S>(
bloc: _bloc,
listenWhen: widget.buildWhen,
listener: (context, state) => setState(() => _state = state),
child: widget.build(context, _state),
);
}
}

The key insight here: BlocBuilder delegates to BlocListener under the hood! The buildWhen callback is passed as listenWhen to the listener. When a new state arrives:

  1. BlocListener checks if listenWhen (your buildWhen) returns true
  2. If true, the listener calls setState(() => _state = state), triggering a rebuild
  3. If false, the listener doesn't fire, so no setState, no rebuild

This is a powerful optimization, but if you leave buildWhen empty or forget to add it for expensive widgets, you're missing optimization opportunities.

This is what avoid-empty-build-when catches.

❌ Bad: BlocBuilder without buildWhen might rebuild too often
BlocBuilder<CounterBloc, int>(
// 🚨 No buildWhen means this rebuilds on EVERY state change
// If the builder is expensive, this could cause jank
builder: (context, state) {
return ExpensiveWidget(count: state);
},
);

Add an explicit buildWhen to control when rebuilds happen:

✅ Good: Explicit buildWhen for optimization
BlocBuilder<CounterBloc, int>(
buildWhen: (previous, current) {
// ✅ Only rebuild when the count actually changes
return previous != current;
},
builder: (context, state) {
return ExpensiveWidget(count: state);
},
);

Dart 3 Type Safety

Dart 3 introduced sealed classes, and they're a game-changer for BLoC patterns. These rules leverage Dart 3's type system to catch bugs at compile time rather than runtime.

Sealed Events and States

Dart 3's sealed classes enable exhaustiveness checking. If your events and states are sealed, the compiler will warn you when you add a new event but forget to handle it, or when your switch statement doesn't cover all states.

This is what prefer-sealed-bloc-events and prefer-sealed-bloc-state enforce.

❌ Bad: No compile-time exhaustiveness checking
// Without sealed, nothing stops you from forgetting a case
abstract class CounterEvent {}

class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
class ResetEvent extends CounterEvent {} // Added later...

// In a widget somewhere:
void handleEvent(CounterEvent event) {
if (event is IncrementEvent) {
// handle
} else if (event is DecrementEvent) {
// handle
}
// 💥 Oops! ResetEvent is silently ignored. No warning.
}

With sealed classes, the compiler enforces exhaustive handling:

✅ Good: Sealed classes enable exhaustiveness checking
sealed class CounterEvent {}

final class IncrementEvent extends CounterEvent {}
final class DecrementEvent extends CounterEvent {}
final class ResetEvent extends CounterEvent {}

// Now this is a compile error:
void handleEvent(CounterEvent event) {
switch (event) {
case IncrementEvent():
// handle
case DecrementEvent():
// handle
// ❌ Compile error: The type 'CounterEvent' is not exhaustively matched
// by the switch cases since it doesn't match 'ResetEvent()'.
}
}

You can configure the naming pattern:

analysis_options.yaml
dcm:
rules:
- prefer-sealed-bloc-events:
name-pattern: Event$
- prefer-sealed-bloc-state:
name-pattern: State$

Immutable Events

If an event can be modified after it's created, you risk mutation-based side effects. An event handler might change a property, affecting how subsequent handlers (or replays) process the same event.

This is what prefer-immutable-bloc-events catches.

❌ Bad: Mutable events can be modified after dispatch
class UpdateUserEvent extends UserEvent {
String name; // 🚨 Mutable field

UpdateUserEvent(this.name);
}

// Somewhere in your code:
final event = UpdateUserEvent('Alice');
bloc.add(event);

// Later, accidentally or intentionally:
event.name = 'Bob'; // 💥 Mutated after dispatch!
// If event handlers cache or compare events, this causes chaos.

Make events immutable with final fields and const constructors:

✅ Good: Immutable events are predictable

sealed class UserEvent {}


final class UpdateUserEvent extends UserEvent {
final String name; // ✅ Final field

const UpdateUserEvent(this.name);
}

Design Patterns in BLoC

When you look at the BLoC source code, you can spot several design patterns at work:

PatternWhere It AppearsPurpose
ObserverBlocObserver, stream.listen()React to state changes without tight coupling
CommandEvent classesEncapsulate actions as objects for replay, logging
MediatorBlocProviderDecouple BLoC creation from widget tree
StrategyEventTransformerSwap event processing strategies (debounce, throttle)
Immutable Value ObjectChange, Transition, StatesEnsure predictable, traceable state history

Understanding these patterns explains why the BLoC architecture works and why breaking its contracts (public methods, mutable events, coupled BLoCs) causes problems.

Code Style Rules

These rules don't prevent bugs, but they make your codebase more consistent and easier to maintain.

prefer-multi-bloc-provider

When you need multiple BLoCs at the same level, nesting BlocProvider widgets creates an indentation nightmare. MultiBlocProvider is syntactic sugar that keeps things flat.

❌ Bad: Nesting hell
BlocProvider<AuthBloc>(
create: (context) => AuthBloc(),
child: BlocProvider<SettingsBloc>(
create: (context) => SettingsBloc(),
child: BlocProvider<ThemeBloc>(
create: (context) => ThemeBloc(),
child: BlocProvider<AnalyticsBloc>(
create: (context) => AnalyticsBloc(),
child: const MyApp(), // 4 levels deep!
),
),
),
);

Use MultiBlocProvider to keep things flat:

✅ Good: Flat and readable
MultiBlocProvider(
providers: [
BlocProvider<AuthBloc>(create: (context) => AuthBloc()),
BlocProvider<SettingsBloc>(create: (context) => SettingsBloc()),
BlocProvider<ThemeBloc>(create: (context) => ThemeBloc()),
BlocProvider<AnalyticsBloc>(create: (context) => AnalyticsBloc()),
],
child: const MyApp(),
);

prefer-bloc-extensions

Use context.read<T>() instead of BlocProvider.of<T>(context). The extension methods are shorter, more consistent, and make it harder to forget listen: true when you need watch semantics.

❌ Bad: Verbose and easy to forget listen parameter
// Have to remember listen: false is default (read behavior)
final bloc = BlocProvider.of<CounterBloc>(context);

// Easy to forget listen: true when you need reactivity
final bloc = BlocProvider.of<CounterBloc>(context, listen: true);

The extension methods are clearer and more concise:

✅ Good: Clear intent, shorter code
// Read once (doesn't rebuild on state changes)
final bloc = context.read<CounterBloc>();

// Watch (rebuilds when state changes)
final bloc = context.watch<CounterBloc>();

prefer-bloc-event-suffix & prefer-bloc-state-suffix

When events are named FetchUsers instead of FetchUsersEvent, it becomes harder to distinguish events from methods, classes, or other constructs at a glance.

❌ Bad: Inconsistent naming
// Is this a class? A method? An event?
class FetchUsers {}
class UpdateProfile {}
class Loading {}

Consistent suffixes make the intent immediately clear:

✅ Good: Clear suffixes
// Immediately clear what these are
class FetchUsersEvent {}
class UpdateProfileEvent {}
class LoadingState {}

You can customize the pattern:

analysis_options.yaml
dcm:
rules:
- prefer-bloc-event-suffix:
name-pattern: Event$
ignore-subclasses: true
- prefer-bloc-state-suffix:
name-pattern: State$
ignore-subclasses: true

Here's a starting analysis_options.yaml configuration that enables all BLoC rules with sensible defaults:

analysis_options.yaml
dcm:
rules:
# Crash & leak prevention
- check-is-not-closed-after-async-gap
- avoid-existing-instances-in-bloc-provider
- avoid-instantiating-in-bloc-value-provider
- avoid-passing-build-context-to-blocs

# Architectural integrity
- avoid-bloc-public-methods
- avoid-bloc-public-fields
- emit-new-bloc-state-instances
- avoid-passing-bloc-to-bloc

# Modern type safety (Dart 3+)
- prefer-sealed-bloc-events:
name-pattern: Event$
- prefer-sealed-bloc-state:
name-pattern: State$
- prefer-immutable-bloc-events:
name-pattern: Event$
- prefer-immutable-bloc-state:
name-pattern: State$

# Code style
- prefer-multi-bloc-provider
- prefer-bloc-extensions
- avoid-empty-build-when
- prefer-bloc-event-suffix:
name-pattern: Event$
ignore-subclasses: true
- prefer-bloc-state-suffix:
name-pattern: State$
ignore-subclasses: true

Conclusion

Reading source code is one of the best ways to level up as a developer. The next time you encounter a bug or wonder why a pattern exists, open the source and look for yourself. You might be surprised what you find.

Happy coding!

Enjoying this article?

Subscribe to get our latest articles and product updates by email.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

Deploying Python apps on Azure App Service

1 Share
From: Microsoft Azure Developers
Duration: 11:15
Views: 24

In this video, Scott and Nir will go through the simplest way to get started with Python in the cloud. The video explores different deployment approaches and recent changes to the platform to be even simpler.

🌮 Chapter Markers:
00:00 - 00:12 – Introduction by Scott
00:12 – 08:12 - Deploying to App Service with uv
08:12 - 09:47 - Container Terminal Access
09:47 - 11:11 - Summary and where to learn more

🌮 Resources
Learn Docs: https://learn.microsoft.com/en-us/azure/app-service/overview
Azure Product page: https://azure.microsoft.com/en-us/products/app-service/


🌮 Follow us on social:
Scott Hanselman | @SHanselman – https://x.com/SHanselman
Azure Friday | @AzureFriday – https://x.com/AzureFriday

Blog - https://aka.ms/azuredevelopers/blog
Twitter - https://aka.ms/azuredevelopers/twitter
LinkedIn - https://aka.ms/azuredevelopers/linkedin
Twitch - https://aka.ms/azuredevelopers/twitch

#azuredeveloper #azure

Read the whole story
alvinashcraft
2 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

What Vibe Coding is Turning Into

1 Share
From: AIDailyBrief
Duration: 10:41
Views: 1,476

Perplexity Computer (Enterprise and Personal) and Replit Agent Four showcase a shift toward persistent, multi-agent workflows and extensible vibecoding canvases. Major themes include multi-model orchestration, always-on local context via Mac Mini integration, parallel task execution, multiplayer collaboration, and usage-based enterprise pricing. Key implications are agent-centric IDEs, intensified security debates over system access, and faster company pivots as AI expands into broader knowledge work.

The AI Daily Brief helps you understand the most important news and discussions in AI.
Subscribe to the podcast version of The AI Daily Brief wherever you listen: https://pod.link/1680633614
Get it ad free at http://patreon.com/aidailybrief
Learn more about the show https://aidailybrief.ai/

Read the whole story
alvinashcraft
2 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

AI Agentic Toolchains Explained - What are Instructions, Skills, SubAgents & MCPs!

1 Share
From: UnoPlatform
Duration: 24:11
Views: 8

AI is changing how software development happens. But Agentic workflows have lots of things to consider.
Do you use some or all things? Let's understand.

- Instructions Agentic guidance/coding standards
- Skills for specific ways of accomplishing tasks
- SubAgents for isolated smaller sub-tasks
- Contextual MCP tools for specific platform needs
- Agents Teams/Squads for higher orchestrations

It's not MCPs vs Skills vs SubAgents. Developers can use as much or as little or a combination - essentially whatever setup works best.

Learn more:
Uno Platform: https://platform.uno/
Documentation: https://platform.uno/docs/articles/intro.html
Discord: https://discord.com/invite/XjsmQHdKfq

Read the whole story
alvinashcraft
2 minutes ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories