bloc_ease 1.2.0
bloc_ease: ^1.2.0 copied to clipboard
A dart library to solve boilerplate issues with flutter_bloc by just using typedefs instead of defining state classes.
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
- Templates
- Tips and Tricks
- Example projects
- 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.,
SucceedState<Auth>
vsSucceedState<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 AuthSucceedState = SucceedState<Auth>
). Snippets are included for IntelliJ and VSCode.
Readme #
The states InitialState
, LoadingState
, SucceedState
, and FailedState
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
SucceedState
orFailedState
:- Calculation (
SucceedState<double>(10)
vsFailedState<double>(DivideByZeroException())
)
- Calculation (
-
Specific State Representation: Some states can only be depicted as
SucceedState
:- Flutter's default counter app state
SucceedState<int>(0)
- Selecting app currency
SucceedState<Currency>(USD())
or unit of temperatureSucceedState<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 FailedState
.
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 UserSucceedState = SucceedState<User>;
typedef UserFailedState = FailedState<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 {
emit(const UserLoadingState());
try {
final user = userRepo.fetchUser();
emit(UserSucceedState(user));
} catch (e) {
emit(UserFailedState('Failed to fetch user', e));
}
}
}
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 succeedBuilder required field.
All other states (InitialState
, LoadingState
, and FailedState
) 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
succeedBuilder: (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
, exSucceedState
, and exFailedState
.
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 CurrentUserSucceedState = SucceedState<User>;
typedef CurrentUserFailedState = FailedState<User>;
...
class SomeWidget extends StatelessWidget {
const SomeWidget({super.key});
@override
Widget build(BuildContext context) {
final currentUserCubit = context.read<CurrentUserCubit>();
return CurrentUserBlocEaseListener(
succeedListener: (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 theexSucceedState
with the currentsucceedState
.
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
FailedState
. - Displaying a success snackbar only when all cubits emit a
SucceedState
. - ...
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 FailedState)) {
showErrorDialog();
} else if(states.every((e) => e is SucceedState)) {
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<SucceedState>( //<-- If you just want to handle SucceedState
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
FailedState
, 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 areSucceedState
, 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 exit BlocEaseState
final blocEaseBlocs = [context.read<UserBloc>(), context.read<OrdersBloc>(), context.read<ReturnsBloc>(), context.read<WishlistBloc>()];
return BlocEaseMultiStateBuilder( //<-- If you just want to handle SucceedState
blocEaseBlocs: blocEaseBlocs,
successBuilder: (states) => Dashboard(),
);
}
}
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$SucceedState = SucceedState<$SuccessType$>; typedef $BlocName$FailedState = FailedState<$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$SucceedState = SucceedState<$SuccessType$>; typedef $CubitName$FailedState = FailedState<$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}>;",
"",
"typedef ${1}InitialState = InitialState<${2}>;",
"typedef ${1}LoadingState = LoadingState<${2}>;",
"typedef ${1}SucceedState = SucceedState<${2}>;",
"typedef ${1}FailedState = FailedState<${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}>;",
"",
"class ${1}Bloc extends Bloc<${1}Event,${1}State> {",
"\t${1}Bloc() : super(const ${1}InitialState());",
"",
"\t${3}",
"}",
]
},
"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:CubitName}State = BlocEaseState<${2:SuccessType}>;",
"",
"typedef ${1}InitialState = InitialState<${2}>;",
"typedef ${1}LoadingState = LoadingState<${2}>;",
"typedef ${1}SucceedState = SucceedState<${2}>;",
"typedef ${1}FailedState = FailedState<${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}>;",
"",
"class ${1}Cubit extends Cubit<${1}State> {",
" ${1}Cubit() : super(const ${1}InitialState());",
"",
" $3",
"}"
]
}
}
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]) {},
succeedListener: (user) {},
child: //..//,
);
}
}
class BlocEaseConsumerExampleWidget extends StatelessWidget {
const BlocEaseConsumerExampleWidget({super.key});
@override
Widget build(BuildContext context) {
// Other than succeedBuilder, all fields are optional.
return UserBlocEaseConsumer( //<-- <CubitName>BlocEaseConsumer
initialListener: () {},
loadingListener: ([progress]) {},
failureListener: ([message, exception, retryCallback]) {},
succeedListener: (user) {},
initialBuilder: () {},
loadingBuilder: ([progress]) {},
failureBuilder: ([message, exception, retryCallback]) ={},
succeedBuilder: (user) => SomeWidget(user),
);
}
}
Work with Bloc #
Use the shortcut bloceasebloc
from the template to create a bloc based on your need with all the typedefs defined for you.
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 UserSucceedState = SucceedState<User>;
typedef UserFailedState = FailedState<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 UserSucceedState = SucceedState<(User, String)>;
typedef UserFailedState = FailedState<(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 UserSucceedState after fetching user',
setUp: () {
when(repo.fetchUser).thenAnswer((_) async => mockUser);
},
build: () => UserCubit(repository: repo),
act: (cubit) => cubit.fetchUser(),
expect: () => UserSucceedState(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: //..//,
);
}
}
Example projects #
These example projects are taken from the official flutter_bloc package examples. So that its easy to compare the implementation. Also it passes all the test cases.
Features and bugs #
Please file feature requests and bugs at the issue tracker.