bloc_ease
A dart library to solve boilerplate issues with flutter_bloc by just using typedefs instead of defining state classes.
Index
- Problems this library addresses
- Solutions this library provides
- Readme
- How to use?
- Example Snippets
- Cache State with Ease - CacheExBlocEaseStateMixin
- Listen to multiple Blocs - BlocEaseMultiStateListener
- Multi state builder - BlocEaseMultiStateBuilder
- Debounce State Emissions - StateDebounce Mixin
- Utilities - BlocEaseUtil
- Templates
- Tips and Tricks
- Connect with me
Problems This Library Addresses
- Repeatedly writing the same types of states for every Bloc/Cubit (Initial, Loading, Success, Failure).
- Overriding
==
andhashCode
, or using the Equatable package for all states. - Handling every state in the UI, even when only the success state is needed.
- Returning the same widget for the same kind of state across all Blocs/Cubits (e.g.,
ProgressIndicator
for the Loading state). - Managing
buildWhen
to avoid handling every state. - Adopting poor practices such as using a single-state class instead of inheritance.
- Managing multiple states together due to boilerplate code.
We are going to solve these using
- Generics (Inherited states)
- InheritedWidget (Global state widgets)
- Builders
- typedefs (Use templates) Don't worry about any of these. This package will take care of everything.
Solutions This Library Provides
- Elimination of the need to write state classes for any Bloc/Cubit. Instead, utilize the states provided by this package with generics (e.g.,
SuccessState<Auth>
vsSuccessState<User>
). - Global handling of common states such as Initial, Loading, and Failure states in the UI. This removes the necessity to manage these states wherever Bloc/Cubit is used.
- Provision of a builder that offers the success object in a type-safe manner, while autonomously handling other states.
- Utilization of typedefs to easily differentiate between states (e.g.,
typedef AuthSuccessState = SuccessState<Auth>
). Snippets are included for IntelliJ and VSCode.
Readme
The states InitialState
, LoadingState
, SuccessState
, and FailureState
can encapsulate most state. If a state cannot be represented within these states, it is likely that multiple states are being managed together.
-
Asynchronous CRUD Operation State: Typically falls into one of these four states:
- Backend fetching
- Device I/O Job
- Multi-threaded operations
-
Synchronous Operation State: Can be one of three states, excluding
LoadingState
:- Parsing logic
- Encryption/Decryption logic
- Filtering a list based on a condition
-
Synchronous Operation: Can be represented by either
SuccessState
orFailureState
:- Calculation (
SuccessState<double>(10)
vsFailureState<double>(DivideByZeroException())
)
- Calculation (
-
Specific State Representation: Some states can only be depicted as
SuccessState
:- Flutter's default counter app state
SuccessState<int>(0)
- Selecting app currency
SuccessState<Currency>(USD())
or unit of temperatureSuccessState<TemperatureUnit>(Celsius())
- Flutter's default counter app state
How to use?
Step 1 - Configuring BlocEaseStateWidgetProvider
BlocEaseStateWidgetProvider
is used to configure the default widgets for InitialState
, LoadingState
, and FailureState
.
Ensure that this widget is wrapped around the MaterialApp
so that it is accessible from everywhere.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return BlocEaseStateWidgetProvider( // <--
initialStateBuilder: (state) => const Placeholder(),
loadingStateBuilder: (state) => const Center(child: CircularProgressIndicator()),
failureStateBuilder: (state) => Center(child: Text(state.message ?? 'Oops something went wrong!')),
child: MaterialApp(
//..
),
);
}
}
Step 2 - Create Bloc/Cubit with the snippet/template provided below.
Use the shortcut bloceasebloc
or bloceasecubit
from the template to create a bloc or cubit based on the need. This creates a template that requires editing two names:
Cubit name -> UserCubit
Success Object -> User (This is the object expected from the success state of the bloc/cubit)
import 'package:bloc_ease/bloc_ease.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
typedef UserState = BlocEaseState<User>; // <-- Success Object
typedef UserInitialState = InitialState<User>;
typedef UserLoadingState = LoadingState<User>;
typedef UserSuccessState = SuccessState<User>;
typedef UserFailureState = FailureState<User>;
typedef UserBlocBuilder = BlocBuilder<UserCubit, UserState>;
typedef UserBlocListener = BlocListener<UserCubit, UserState>;
typedef UserBlocConsumer = BlocConsumer<UserCubit, UserState>;
typedef UserBlocEaseBuilder = BlocEaseStateBuilder<UserCubit, User>;
typedef UserBlocEaseListener = BlocEaseStateListener<UserCubit, User>;
typedef UserBlocEaseConsumer = BlocEaseStateConsumer<UserCubit, User>;
class UserCubit extends Cubit<UserState> { //<--Cubit name
UserCubit(this.userRepo)
: super(const UserInitialState());
final UserRepo userRepo;
void fetchUser() async {
emitLoading(); // Simplified state emission
try {
final user = userRepo.fetchUser();
emitSuccess(user); // Simplified state emission
} catch (e) {
emitFailure('Failed to fetch user', e); // Simplified state emission
}
}
}
> **Note:** The `emitLoading()`, `emitSuccess()`, `emitFailure()`, and `emitInitial()` methods use `safeEmit()` internally, which checks if the bloc is closed before emitting a state. This prevents common errors when emitting states after a bloc has been closed. These extension methods are available on any BlocBase that emits BlocEaseState.
Step 3 - Use <CubitName>BlocEaseBuilder
instead of BlocBuilder in the UI
<CubitName>BlocEaseBuilder (UserBlocEaseBuilder)
is the builder used to access the Success Object configured in Step 2 with the successBuilder required field.
All other states (InitialState
, LoadingState
, and FailureState
) use the default widgets configured in Step 1.
class SomeWidget extends StatelessWidget {
const SomeWidget({super.key});
@override
Widget build(BuildContext context) {
return UserBlocEaseBuilder( //<-- <CubitName>BlocEaseBuilder
successBuilder: (user) //<-- This provides the Success Object we configured in the Step 2.
=> SomeOtherWidget(user),
);
}
}
Example Snippets
Fetching user details
Fetching user usually needs 4 states.
- Initial state - When not logged in
- Loading state - When fetching in progress
- Succeed state - When successfully fetched
- Failed state - User not available / Failed to fetch
Fetching item details on opening item page
Since we need to fetch the item on opening the page, this usually holds 3 states.
- Loading state - When fetching in progress
- Succeed state - when item fetched successfully
- Failed state - When failed to fetch item
Notice that, ItemInitialState
not used even though it can be accessed.
Cache State with Ease - CacheExBlocEaseStateMixin
By utilizing the CacheExBlocEaseStateMixin
mixin with any Bloc or Cubit that emits BlocEaseState
, you gain access to previous states, including exLoadingState
, exSuccessState
, and exFailureState
.
Additionally, the exSucceedObject
allows direct access to the previous success object, if it exists. These extended states enable comparison and operations based on state changes.
import 'package:bloc_ease/bloc_ease.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
typedef CurrentUserState = BlocEaseState<User>;
// -- Uses Mixin CacheExBlocEaseStateMixin
class CurrentUserCubit extends Cubit<CurrentUserState> with CacheExBlocEaseStateMixin {
CurrentUserCubit()
: super(const CurrentUserInitialState());
void doSomething() {
final userId = exSucceedObject?.id; //<-- We can access exSucceedObject
if(userId != null) {
...
}
}
void resetState() {
emit(const CurrentUserInitialState());
resetCache(); //<-- (Can call resetCache method to force reset cache)
}
}
typedef CurrentUserInitialState = InitialState<User>;
typedef CurrentUserLoadingState = LoadingState<User>;
typedef CurrentUserSuccessState = SuccessState<User>;
typedef CurrentUserFailureState = FailureState<User>;
...
class SomeWidget extends StatelessWidget {
const SomeWidget({super.key});
@override
Widget build(BuildContext context) {
final currentUserCubit = context.read<CurrentUserCubit>();
return CurrentUserBlocEaseListener(
successListener: (user) {
final exUser = currentUserCubit.exSucceedObject; //<-- Can access exSucceedObject
if(exUser?.email == null && user.email != null) {
welcomeUserViaEmail(user.email);
}
},
child: ...
);
}
}
Tip: It is possible to animate transitions between loading states or success states by comparing the
exLoadingState
with the currentloadingState
or theexSuccessState
with the currentsuccessState
.
Listen to multiple Blocs - BlocEaseMultiStateListener
BlocEaseMultiStateListener allows for monitoring multiple blocs or cubits that emit BlocEaseState
. The primary use cases include:
- Displaying a progress dialog when any cubit is in the
LoadingState
. - Showing an error message if any cubit emits a
FailureState
. - Displaying a success snackbar only when all cubits emit a
SuccessState
. - ...
class SomeWidget extends StatelessWidget {
const SomeWidget({super.key});
@override
Widget build(BuildContext context) {
// Remember: Both AuthBloc and UserBloc should emit BlocEaseState
final blocEaseBlocs = [context.read<AuthBloc>(), context.read<UserBloc>()];
return BlocEaseMultiStateListener(
blocEaseBlocs: blocEaseBlocs,
onStateChange: (states) {
if(states.any((e) => e is LoadingState)) {
showLoadingDialog();
} else if(states.any((e) => e is FailureState)) {
showErrorDialog();
} else if(states.every((e) => e is SuccessState)) {
showSuccessSnackBar();
}
},
child: ...,
);
}
}
PRO TIP: If you want to handle only one state, you can simply use Generics like
class SomeWidget extends StatelessWidget {
const SomeWidget({super.key});
@override
Widget build(BuildContext context) {
// Remember: Both AuthBloc and UserBloc should emit BlocEaseState
final blocEaseBlocs = [context.read<AuthBloc>(), context.read<UserBloc>()];
return BlocEaseMultiStateListener<SuccessState>( //<-- If you just want to handle SuccessState
blocEaseBlocs: blocEaseBlocs,
onStateChange: (states) => showSuccessSnackBar(),
child: ...,
);
}
}
Multi-State Builder - BlocEaseMultiStateBuilder
The BlocEaseMultiStateBuilder
allows for the combination of different blocs or cubits that emit BlocEaseState
into a single widget. This utility offers several use cases, including:
- Displaying a single loading indicator instead of one for each bloc.
- Showing a single error widget instead of multiple error widgets on the screen.
- Indicating loading progress by knowing how many blocs or cubits are in the
LoadingState
(automatically handled withBlocEaseStateWidgetProvider
-progress
field). - Rendering all widgets at once instead of loading them separately.
By default, only the successBuilder
needs to be provided; all other states are managed by default with BlocEaseStateWidgetProvider
.
Note: If any state is a
FailureState
, an error widget is displayed. If any state is anInitialState
, the initial widget is shown. If any state is aLoadingState
, the loading widget is rendered. Only if all states areSuccessState
, the success widget is displayed.
class SomeWidget extends StatelessWidget {
const SomeWidget({super.key});
@override
Widget build(BuildContext context) {
// Remember: All of these Bloc/Cubit should emit BlocEaseState
final blocEaseBlocs = [context.read<UserBloc>(), context.read<OrdersBloc>(), context.read<ReturnsBloc>(), context.read<WishlistBloc>()];
return BlocEaseMultiStateBuilder( //<-- If you just want to handle SuccessState
blocEaseBlocs: blocEaseBlocs,
successBuilder: (states) => Dashboard(),
);
}
}
Debounce State Emissions - StateDebounce Mixin
The StateDebounce
mixin provides debouncing functionality for state emissions. This is useful for scenarios like search functionality where you want to wait for the user to stop typing before making an API call, avoiding unnecessary network requests or computations.
Using with Cubit
class SearchCubit extends Cubit<BlocEaseState<List<String>>> with StateDebounce {
SearchCubit() : super(const InitialState());
void search(String query) {
debounce(() async {
emitLoading(); // Simplified state emission
try {
final results = await searchApi.search(query);
emitSuccess(results); // Simplified state emission
} catch (e) {
emitFailure(e.toString()); // Simplified state emission
}
});
}
}
Using with Bloc
class SearchBloc extends Bloc<SearchEvent, BlocEaseState<List<String>>> with StateDebounce {
SearchBloc() : super(const InitialState()) {
on<SearchQueryChanged>((event, emit) {
debounce(() async {
emit(const LoadingState());
try {
final results = await searchApi.search(event.query);
emit(SuccessState(results));
} catch (e) {
emit(FailureState(e.toString()));
}
});
});
}
}
The default debounce duration is 300 milliseconds, but you can customize it by providing a specific duration:
debounce(() {
// Your code here
}, Duration(milliseconds: 500));
PRO TIP: For more complex debouncing scenarios in Bloc, consider using event transformers instead of this mixin.
Utilities - BlocEaseUtil
The BlocEaseUtil
class provides utility methods for working with BlocEase states.
Waiting for Loading to Complete
The waitUntilLoading
method allows you to wait until a BlocEase bloc is no longer in a loading state. This is useful when you need to perform an action only after loading is complete.
class SomeWidget extends StatelessWidget {
const SomeWidget({super.key});
@override
Widget build(BuildContext context) {
final userCubit = context.read<UserCubit>();
return ElevatedButton(
onPressed: () async {
userCubit.fetchUser();
// Wait until the loading is complete
final state = await BlocEaseUtil.waitUntilLoading(userCubit);
if (state is SuccessState) {
showSuccessDialog();
} else if (state is FailureState) {
showErrorDialog();
}
},
child: const Text('Fetch User'),
);
}
}
This method returns a Future
that completes with the current state when it is no longer in the loading state, making it easy to perform actions based on the final state after loading.
Using with RefreshIndicator
A practical use case for waitUntilLoading
is with a RefreshIndicator
widget to ensure proper handling of loading states:
class ProductListScreen extends StatelessWidget {
const ProductListScreen({super.key});
@override
Widget build(BuildContext context) {
final productListCubit = context.read<ProductListCubit>();
return Scaffold(
appBar: AppBar(title: const Text('Products')),
body: RefreshIndicator(
onRefresh: () async {
// Trigger the refresh
productListCubit.fetchProducts();
// Wait until loading is complete before hiding the RefreshIndicator
await BlocEaseUtil.waitUntilLoading(productListCubit);
},
child: ProductListBlocEaseBuilder(
successBuilder: (products) {
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) => ProductItem(product: products[index]),
);
},
),
),
);
}
}
This ensures the RefreshIndicator continues to show until the loading state is complete, providing a better user experience when refreshing list data.
Templates
Intellij and Android Studio
Copy both templates at once -> Intellij/Android studio Settings -> Live Templates -> Create new template group as BlocEase -> Paste
<template name="bloceasebloc" value="import 'package:bloc_ease/bloc_ease.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; typedef $BlocName$State = BlocEaseState<$SuccessType$>; class $BlocName$Bloc extends Bloc<$BlocName$Event,$BlocName$State> { $BlocName$Bloc() : super(const $BlocName$InitialState()); $Dependencies$ void $FunctionName$() { emit(const $BlocName$LoadingState()); $ImplementationStart$ } } typedef $BlocName$InitialState = InitialState<$SuccessType$>; typedef $BlocName$LoadingState = LoadingState<$SuccessType$>; typedef $BlocName$SuccessState = SuccessState<$SuccessType$>; typedef $BlocName$FailureState = FailureState<$SuccessType$>; typedef $BlocName$BlocBuilder = BlocBuilder<$BlocName$Bloc, $BlocName$State>; typedef $BlocName$BlocListener = BlocListener<$BlocName$Bloc, $BlocName$State>; typedef $BlocName$BlocConsumer = BlocConsumer<$BlocName$Bloc, $BlocName$State>; typedef $BlocName$BlocEaseBuilder = BlocEaseStateBuilder<$BlocName$Bloc, $SuccessType$>; typedef $BlocName$BlocEaseListener = BlocEaseStateListener<$BlocName$Bloc, $SuccessType$>; typedef $BlocName$BlocEaseConsumer = BlocEaseStateConsumer<$BlocName$Bloc, $SuccessType$>;" description="BlocEase Four state bloc template" toReformat="false" toShortenFQNames="true">
<variable name="BlocName" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="SuccessType" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="Dependencies" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="FunctionName" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="ImplementationStart" expression="" defaultValue="" alwaysStopAt="true" />
<context>
<option name="DART" value="true" />
<option name="FLUTTER" value="true" />
</context>
</template>
<template name="bloceasecubit" value="import 'package:bloc_ease/bloc_ease.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; typedef $CubitName$State = BlocEaseState<$SuccessType$>; class $CubitName$Cubit extends Cubit<$CubitName$State> { $CubitName$Cubit() : super(const $CubitName$InitialState()); $Dependencies$ void $FunctionName$() { emit(const $CubitName$LoadingState()); $ImplementationStart$ } } typedef $CubitName$InitialState = InitialState<$SuccessType$>; typedef $CubitName$LoadingState = LoadingState<$SuccessType$>; typedef $CubitName$SuccessState = SuccessState<$SuccessType$>; typedef $CubitName$FailureState = FailureState<$SuccessType$>; typedef $CubitName$BlocBuilder = BlocBuilder<$CubitName$Cubit, $CubitName$State>; typedef $CubitName$BlocListener = BlocListener<$CubitName$Cubit, $CubitName$State>; typedef $CubitName$BlocConsumer = BlocConsumer<$CubitName$Cubit, $CubitName$State>; typedef $CubitName$BlocEaseBuilder = BlocEaseStateBuilder<$CubitName$Cubit, $SuccessType$>; typedef $CubitName$BlocEaseListener = BlocEaseStateListener<$CubitName$Cubit, $SuccessType$>; typedef $CubitName$BlocEaseConsumer = BlocEaseStateConsumer<$CubitName$Cubit, $SuccessType$>; " description="BlocEase Four state cubit template" toReformat="false" toShortenFQNames="true">
<variable name="CubitName" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="SuccessType" expression="" defaultValue="SuccessType" alwaysStopAt="true" />
<variable name="Dependencies" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="FunctionName" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="ImplementationStart" expression="" defaultValue="" alwaysStopAt="true" />
<context>
<option name="DART" value="true" />
<option name="FLUTTER" value="true" />
</context>
</template>
VSCode (TODO: Change and test)
Copy -> VSCode -> Cmd(Ctrl) + Shift + P -> "Snippets: Configure User Snippets" -> dart.json -> Paste
{
"BlocEase Bloc": {
"prefix": ["bloceasebloc"],
"description": "BlocEase Four state bloc template",
"body": [
"import 'package:bloc_ease/bloc_ease.dart';",
"import 'package:flutter_bloc/flutter_bloc.dart';",
"",
"typedef ${1:BlocName}State = BlocEaseState<${2:SuccessType}>;",
"",
"class ${1}Bloc extends Bloc<${1}Event,${1}State> {",
"\t${1}Bloc() : super(const ${1}InitialState());",
"",
"\t${3:BlocBody}",
"}",
"",
"typedef ${1}InitialState = InitialState<${2}>;",
"typedef ${1}LoadingState = LoadingState<${2}>;",
"typedef ${1}SuccessState = SuccessState<${2}>;",
"typedef ${1}FailureState = FailureState<${2}>;",
"",
"typedef ${1}BlocBuilder = BlocBuilder<${1}Bloc, ${1}State>;",
"typedef ${1}BlocListener = BlocListener<${1}Bloc, ${1}State>;",
"typedef ${1}BlocConsumer = BlocConsumer<${1}Bloc, ${1}State>;",
"",
"typedef ${1}BlocEaseBuilder = BlocEaseStateBuilder<${1}Bloc, ${2}>;",
"typedef ${1}BlocEaseListener = BlocEaseStateListener<${1}Bloc, ${2}>;",
"typedef ${1}BlocEaseConsumer = BlocEaseStateConsumer<${1}Bloc, ${2}>;",
]
},
"BlocEase Cubit": {
"prefix": ["bloceasecubit"],
"description": "BlocEase Four state cubit template",
"body": [
"import 'package:bloc_ease/bloc_ease.dart';",
"import 'package:flutter_bloc/flutter_bloc.dart';",
"",
"typedef ${1:BlocName}State = BlocEaseState<${2:SuccessType}>;",
"",
"class ${1}Cubit extends Cubit<${1}State> {",
"\t${1}Cubit() : super(const ${1}InitialState());",
"",
"\t${3:CubitBody}",
"}",
"",
"typedef ${1}InitialState = InitialState<${2}>;",
"typedef ${1}LoadingState = LoadingState<${2}>;",
"typedef ${1}SuccessState = SuccessState<${2}>;",
"typedef ${1}FailureState = FailureState<${2}>;",
"",
"typedef ${1}BlocBuilder = BlocBuilder<${1}Cubit, ${1}State>;",
"typedef ${1}BlocListener = BlocListener<${1}Cubit, ${1}State>;",
"typedef ${1}BlocConsumer = BlocConsumer<${1}Cubit, ${1}State>;",
"",
"typedef ${1}BlocEaseBuilder = BlocEaseStateBuilder<${1}Cubit, ${2}>;",
"typedef ${1}BlocEaseListener = BlocEaseStateListener<${1}Cubit, ${2}>;",
"typedef ${1}BlocEaseConsumer = BlocEaseStateConsumer<${1}Cubit, ${2}>;",
]
}
}
Tips and Tricks
Using BlocEaseListener
and BlocEaseConsumer
The template also generates <CubitName>BlocEaseListener
and <CubitName>BlocEaseConsumer
which can be used instead of BlocListener and BlocConsumer.
class BlocEaseListenerExampleWidget extends StatelessWidget {
const BlocEaseListenerExampleWidget({super.key});
@override
Widget build(BuildContext context) {
// All fields are optional
return UserBlocEaseListener( //<-- <CubitName>BlocEaseListener
initialListener: () {},
loadingListener: ([progress]) {},
failureListener: ([message, exception, retryCallback]) {},
successListener: (user) {},
shouldRunOnInit: true, // Run listeners immediately on initialization
child: //..//,
);
}
}
### The `shouldRunOnInit` Parameter
By default, BlocEaseListener only triggers listeners when the state changes after the widget is initialized. Setting `shouldRunOnInit: true` makes the listener react to the current state immediately upon initialization. This is useful in scenarios where you need to perform actions based on the existing state right away.
```dart
class UserProfileScreen extends StatelessWidget {
const UserProfileScreen({super.key});
@override
Widget build(BuildContext context) {
// The user may already be loaded when this screen is built
return UserBlocEaseListener(
shouldRunOnInit: true, // React to current state immediately
successListener: (user) {
// This will run immediately if the user is already loaded
analytics.logProfileView(userId: user.id);
},
failureListener: (message, _, __) {
// Show error immediately if there was a problem loading the user
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message ?? 'Failed to load profile')),
);
},
child: Scaffold(
appBar: AppBar(title: const Text('User Profile')),
body: const UserProfileContent(),
),
);
}
}
class BlocEaseConsumerExampleWidget extends StatelessWidget { const BlocEaseConsumerExampleWidget({super.key});
@override Widget build(BuildContext context) { // Other than successBuilder, all fields are optional. return UserBlocEaseConsumer( //<--
initialBuilder: () {},
loadingBuilder: ([progress]) {},
failureBuilder: ([message, exception, retryCallback]) ={},
successBuilder: (user) => SomeWidget(user),
);
} }
### Work with Bloc
Use the shortcut `bloceasebloc` from the [template](#templates) to create a bloc based on your need with all the typedefs defined for you.
```dart
import 'package:bloc_ease/bloc_ease.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
part 'user_event.dart';
typedef UserState = BlocEaseState<User>;
typedef UserInitialState = InitialState<User>;
typedef UserLoadingState = LoadingState<User>;
typedef UserSuccessState = SuccessState<User>;
typedef UserFailureState = FailureState<User>;
typedef UserBlocBuilder = BlocBuilder<UserBloc, UserState>;
typedef UserBlocListener = BlocListener<UserBloc, UserState>;
typedef UserBlocConsumer = BlocConsumer<UserBloc, UserState>;
typedef UserBlocEaseBuilder = BlocEaseStateBuilder<UserBloc, User>;
typedef UserBlocEaseListener = BlocEaseStateListener<UserBloc, User>;
typedef UserBlocEaseConsumer = BlocEaseStateConsumer<UserBloc, User>;
class UserBloc extends Bloc<UserEvent,UserState> {
UserBloc()
: super(const UserInitialState()){
// on...
}
}
Take advantage of Records when defining SuccessObject type.
In some cases, we need multiple params as Success object. In that case, we could easily take advantage of Records instead of creating a data class for that.
import 'package:bloc_ease/bloc_ease.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
typedef UserState = BlocEaseState<(User, String)>; // <-- Success Object
typedef UserInitialState = InitialState<(User, String)>;
typedef UserLoadingState = LoadingState<(User, String)>;
typedef UserSuccessState = SuccessState<(User, String)>;
typedef UserFailureState = FailureState<(User, String)>;
typedef UserBlocBuilder = BlocBuilder<UserCubit, UserState>;
typedef UserBlocListener = BlocListener<UserCubit, UserState>;
typedef UserBlocConsumer = BlocConsumer<UserCubit, UserState>;
typedef UserBlocEaseBuilder = BlocEaseStateBuilder<UserCubit, (User, String)>;
class UserCubit extends Cubit<UserState> {
UserCubit() : super(const UserInitialState());
//..//
}
Testing
Testing is also totally straight-forward as just using Bloc/Cubit.
blocTest<UserCubit, UserState>(
'emits UserSuccessState after fetching user',
setUp: () {
when(repo.fetchUser).thenAnswer((_) async => mockUser);
},
build: () => UserCubit(repository: repo),
act: (cubit) => cubit.fetchUser(),
expect: () => UserSuccessState(mockUser), //<--
verify: (_) => verify(repository.fetchUser).called(1),
);
Take advantage of all typedefs generated by this template.
One of the painful work with using BlocBuilder is that we need to write the entire boilerplate everytime. Take advantage of the typedefs generated by the template provided.
UserBlocBuilder
instead ofBlocBuilder<UserCubit, UserState>
UserBlocListener
instead ofBlocListener<UserCubit, UserState>
UserBlocConsumer
instead ofBlocConsumer<UserCubit, UserState>
Overriding the default state widgets for a certain page or widget tree
If we wrap the same BlocEaseStateWidgetProvider
over some widget tree, all the default widgets gets overridden with this new implementation.
So all the BlocEaseBuilders comes under this widget use this overridden widgets as default case.
class SomePage extends StatelessWidget {
const SomePage({super.key});
@override
Widget build(BuildContext context) {
return BlocEaseStateWidgetProvider(
initialStateBuilder: () => const SizedBox(),
loadingStateBuilder: ([progress]) => const CustomLoader(),
failureStateBuilder: ([exception, message, retryCallback]) => Text(message ?? 'Oops something went wrong!'),
child: //..//,
);
}
}
Features and bugs
Please file feature requests and bugs at the issue tracker.