flutter
/

BLoC Initial State: Setup, Persistence & Lazy Initialization

Last Sync: Today

On this page

10
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

BLoC Initial State: Setup, Persistence & Lazy Initialization

What is Initial State in BLoC?

In BLoC, the initial state is the state that a BLoC or Cubit starts with before any events are processed. It represents the default state of your feature – often a loading indicator, an empty list, or a pre‑loaded value. Proper management of initial state is crucial for user experience, testing, and maintaining predictable behavior.

Why Initial State Matters

  • User Experience – Shows loading indicators or placeholder content immediately.
  • Predictability – Ensures the app starts in a known state, avoiding unexpected UI.
  • Testing – You can test how the BLoC behaves from a known starting point.
  • Performance – Lazy initialization can defer heavy setup until needed.
  • Persistence – With hydrated_bloc, the initial state can be restored from storage.

Basic Initial State Setup

In a standard BLoC, you pass the initial state to the super constructor of Bloc or Cubit.

DARTRead-only
1
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterState(value: 0)) {
    // ... handlers
  }
}

// For Cubit:
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
}

// Or with a custom state class:
class CounterCubit extends Cubit<CounterState> {
  CounterCubit() : super(const CounterState.loading());
}

Designing Initial States with Loading/Empty States

Often you want to start with a loading or empty state to indicate that data is being fetched. Use a factory constructor or separate state subclasses to represent these states cleanly.

DARTRead-only
1
@immutable
class PostsState extends Equatable {
  const PostsState();
  @override List<Object?> get props => [];
}

class PostsInitial extends PostsState {}
class PostsLoading extends PostsState {}
class PostsLoaded extends PostsState {
  final List<Post> posts;
  const PostsLoaded(this.posts);
  @override List<Object?> get props => [posts];
}

// In BLoC:
class PostsBloc extends Bloc<PostsEvent, PostsState> {
  PostsBloc() : super(PostsInitial()) { ... }
}

Lazy Initialization

Sometimes you don't want to create a BLoC instance until it's actually needed. BlocProvider lazily creates the bloc by default. You can also control lazy creation of dependencies inside the BLoC.

DARTRead-only
1
// Lazy creation – the bloc is created only when accessed
BlocProvider(
  create: (context) => MyBloc(),
  lazy: true, // default
  child: MyWidget(),
);

// Or inside a BLoC, lazy initialization of heavy dependencies
class MyBloc extends Bloc<MyEvent, MyState> {
  late final HeavyService _service;

  MyBloc() : super(MyInitial()) {
    on<InitEvent>((event, emit) {
      _service = HeavyService(); // only created when InitEvent is added
    });
  }
}

Persistent Initial State with HydratedBloc

hydrated_bloc automatically restores the last state from storage when the BLoC is created. If no persisted state exists, it falls back to the initial state you provide. This is perfect for user sessions, settings, or any state that should survive app restarts.

DARTRead-only
1
class SettingsCubit extends HydratedCubit<SettingsState> {
  SettingsCubit() : super(const SettingsState()); // initial state

  @override
  SettingsState fromJson(Map<String, dynamic> json) {
    return SettingsState.fromJson(json);
  }

  @override
  Map<String, dynamic> toJson(SettingsState state) {
    return state.toJson();
  }
}

// When the cubit is created, it checks for stored state.
// If found, it's used as the initial state; otherwise, the constructor's initial state is used.

Initial State from External Sources

Sometimes the initial state depends on async data (e.g., reading from SharedPreferences, fetching a token). You can handle this by emitting a loading state initially, then loading the data in onInit or by dispatching an initial event.

DARTRead-only
1
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final AuthRepository repository;

  AuthBloc({required this.repository}) : super(AuthInitial()) {
    on<AppStarted>((event, emit) async {
      emit(AuthLoading());
      final user = await repository.getCurrentUser();
      if (user != null) {
        emit(AuthAuthenticated(user));
      } else {
        emit(AuthUnauthenticated());
      }
    });
  }
}

// In main:
BlocProvider(
  create: (context) => AuthBloc(repository: repo)..add(AppStarted()),
  child: MyApp(),
);

Best Practices

  • Use meaningful initial states – Represent the actual starting condition (loading, empty, placeholder).
  • Leverage hydrated_bloc for persistent state – Let the user resume where they left off.
  • Avoid expensive work in the initial state constructor – Keep the constructor light; use on<AppStarted> or similar for async initialization.
  • Use Equatable for all states – Ensures correct equality comparisons, especially when restoring from storage.
  • Consider lazy providers – Create blocs only when needed to save resources.
  • Test the initial state – Write tests that verify the initial state is correct.

Common Mistakes

  • ❌ Performing async work in the constructor – Can lead to race conditions and unclosed streams.
  • ❌ Forgetting to handle hydrated_bloc initialization – Without setting HydratedBloc.storage, persistence won't work.
  • ❌ Using a single initial state that's too generic – Using Initial for everything makes it hard to distinguish between fresh start and no data.
  • ❌ Not restoring state correctly in hydrated BLoCs – If fromJson returns a different type than the super initial state, it may be ignored.
  • ❌ Creating blocs with create: (context) => MyBloc()..add(InitEvent()) but forgetting to await? – Ensure async initial events are handled properly.

Conclusion

The initial state sets the stage for your BLoC's behavior. By designing it thoughtfully, you can create apps that start fast, handle persistence elegantly, and provide a seamless user experience. Whether you're using a simple default state or restoring from storage, following best practices will keep your BLoC code clean and maintainable.

Try it yourself

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (_) => DataBloc(),
        child: DataPage(),
      ),
    );
  }
}

// ---------- State ----------
abstract class DataState extends Equatable {
  const DataState();
  @override List<Object?> get props => [];
}

class DataInitial extends DataState {}
class DataLoading extends DataState {}
class DataLoaded extends DataState {
  final String data;
  const DataLoaded(this.data);
  @override List<Object?> get props => [data];
}
class DataError extends DataState {
  final String message;
  const DataError(this.message);
  @override List<Object?> get props => [message];
}

// ---------- Events ----------
abstract class DataEvent extends Equatable {
  const DataEvent();
  @override List<Object?> get props => [];
}

class LoadData extends DataEvent {}

// ---------- BLoC ----------
class DataBloc extends Bloc<DataEvent, DataState> {
  DataBloc() : super(DataInitial()) {
    on<LoadData>((event, emit) async {
      emit(DataLoading());
      await Future.delayed(Duration(seconds: 1));
      emit(DataLoaded('Hello from initial state! 👋'));
    });
  }
}

// ---------- UI ----------
class DataPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Initial State Demo')),
      body: BlocBuilder<DataBloc, DataState>(
        builder: (context, state) {
          if (state is DataInitial) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Initial state: DataInitial', style: TextStyle(fontSize: 18)),
                  SizedBox(height: 20),
                  ElevatedButton(
                    onPressed: () => context.read<DataBloc>().add(LoadData()),
                    child: Text('Load Data'),
                  ),
                ],
              ),
            );
          } else if (state is DataLoading) {
            return Center(child: CircularProgressIndicator());
          } else if (state is DataLoaded) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Loaded: ${state.data}', style: TextStyle(fontSize: 24)),
                  SizedBox(height: 20),
                  ElevatedButton(
                    onPressed: () => context.read<DataBloc>().add(LoadData()),
                    child: Text('Reload'),
                  ),
                ],
              ),
            );
          } else if (state is DataError) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Error: ${state.message}'),
                  ElevatedButton(
                    onPressed: () => context.read<DataBloc>().add(LoadData()),
                    child: Text('Retry'),
                  ),
                ],
              ),
            );
          }
          return Container();
        },
      ),
    );
  }
}

Test Your Knowledge

Q1
of 4

How do you set the initial state for a BLoC?

A
By calling `emit` in the constructor
B
By passing it to the super constructor
C
By overriding the `initialState` getter
D
By using `BlocProvider.initialState`
Q2
of 4

What happens if you use `hydrated_bloc` and a persisted state exists?

A
It overrides the initial state passed to the constructor
B
It merges with the initial state
C
It throws an error
D
It ignores the persisted state
Q3
of 4

How should you handle initial state that requires async data (e.g., loading from disk)?

A
Fetch the data in the constructor
B
Emit a loading state, then fetch data in an event handler
C
Use a FutureBuilder in the UI
D
Pass the data as a parameter to the bloc
Q4
of 4

What is the default behavior of `BlocProvider` regarding lazy initialization?

A
Creates the bloc immediately
B
Creates the bloc lazily when first accessed
C
Never creates the bloc
D
Requires manual creation

Frequently Asked Questions

Can I change the initial state after the BLoC is created?

No, the initial state is set once when the BLoC is instantiated. To update the state later, you emit new states from event handlers.

What's the difference between initial state and a loading state?

Initial state is the very first state before any events are processed. A loading state is often emitted as the first response to an event. In many designs, the initial state is itself a loading state.

How do I initialize a BLoC with data from a repository?

Emit an initial loading state, then in on<AppStarted> (or after creation) fetch the data and emit the loaded state.

Does `hydrated_bloc` override the initial state I pass to the constructor?

If a persisted state exists, it will be used as the initial state instead of the constructor parameter. If no persisted state exists, the constructor state is used.

Should I use `lazy: false` in `BlocProvider`?

Only if you need the bloc to be created immediately (e.g., to start listening to streams). Otherwise, lazy creation is preferred for performance.

Previous

bloc state copywith

Next

bloc buildwhen listenwhen

Related Content

Need help?

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