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.
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.
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.
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.
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.
Best Practices
- Use meaningful initial states – Represent the actual starting condition (loading, empty, placeholder).
- Leverage
hydrated_blocfor 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
Equatablefor 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_blocinitialization – Without settingHydratedBloc.storage, persistence won't work. - ❌ Using a single initial state that's too generic – Using
Initialfor everything makes it hard to distinguish between fresh start and no data. - ❌ Not restoring state correctly in hydrated BLoCs – If
fromJsonreturns 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.