bloc_ease 1.0.0
bloc_ease: ^1.0.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 - BlocEaseStateCacheMixin
- Listen to multiple Blocs - BlocEaseMultiStateListener
- Multi state builder - BlocEaseMultiStateBuilder
- Templates
- Tips and Tricks
- Example projects
- Connect with me
Problems this library addresses #
- Writing same type of states for every blocs / cubits (Initial, Loading, Success, Failure).
- Overriding == and hashcode, or using Equatable package for all states.
- Need to handle every states in the UI even-though we just need success state.
- Return same widget for same kind of state across all blocs / cubits (ProgressIndicator for Loading state).
- Need to handle buildWhen so that we don't need to handle every states.
- Choosing bad practice of using Single-state class instead of Inheritance so that its easy for us to handle in UI.
- Choosing bad practice of managing multiple states together because of boilerplate.
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 #
- Don't need to write state classes for any Bloc / Cubit. Instead using the state comes with this package with generics (
SucceedState<Auth>
vsSucceedState<User>
). - Globally handling common states like Initial, Loading, Failure states in UI. Don't need to worry about these state where-ever we are using Bloc / Cubit.
- Comes with a builder that provides the success object in typesafe manner and it could handle other states by itself.
- Using typedefs to easily differentiate between states (
typedef AuthSucceedState = SucceedState<Auth>
). (Snippet included for Intellij and VSCode)
Readme #
InitialState
LoadingState
SucceedState
FailedState
. Trust me, we could hold any state with one of these states. If we could not hold our state within these states, we are most probably managing multiple states together.
- Asynchronous CRUD Operation state can usually be either of these 4 states.
- Backend fetching
- Device IO Job
- Multi-threaded operations
- Some synchronous Operation state can be either of 3 states other than
LoadingState
.- Parsing logic
- Encryption / Decryption logic
- Filtering a list with some condition
- Some synchronous operation can hold just
SucceedState
orFailedState
.- Calculation (
SucceedState<double>(10)
vsFailedState<double>(DivideByZeroException())
)
- Calculation (
- Some state 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 BlocEaseStateWidgetsProvider
#
BlocEaseStateWidgetsProvider
is used to configure the default widgets for InitialState
, LoadingState
and FailedState
.
Remember, make sure this widget is wrapped over the MaterialApp
so that it is accessible from everywhere.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return BlocEaseStateWidgetsProvider( // <--
initialStateBuilder: () => const Placeholder(),
loadingStateBuilder: ([progress]) => const Center(child: CircularProgressIndicator()),
failureStateBuilder: ([exceptionObject, failureMessage]) => Center(child: Text(failureMessage ?? '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 bloc or cubit based on the need. That creates this template and you just need to edit 2 names.
- Cubit name -> UserCubit
- Success Object -> User (This is the object we expect 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 we can use to access the Success Object we configured in Step 2 with succeedBuilder
required field.
All the other states InitialState
, LoadingState
and FailedState
uses the default widgets we 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 - BlocEaseStateCacheMixin #
Just by using this Mixin BlocEaseStateCacheMixin
with any bloc or cubit that emits BlocEaseState
, we get access to previous states of type with exLoadingState
, exSucceedState
and exFailedState
.
With this exStates, we can compare the change and do operation based on that.
import 'package:bloc_ease/bloc_ease.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
typedef CurrentUserState = BlocEaseState<User>;
// -- Uses Mixin BlocEaseStateCacheMixin
class CurrentUserCubit extends Cubit<CurrentUserState> with BlocEaseStateCacheMixin {
CurrentUserCubit()
: super(const CurrentUserInitialState());
void doSomething() {
final userId = exSucceedState?.success.id; //<-- We can access exSucceedState
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.exSucceedState?.success; //<-- Can access exSucceedState
if(exUser?.email == null && user.email != null) {
welcomeUserViaEmail(user.email);
}
},
child: ...
);
}
}
Tip: We can animate between loading states or success states by comparing
exLoadingState
and currentloadingState
orexSucceedState
and currentsucceedState
.
Listen to multiple Blocs - BlocEaseMultiStateListener #
BlocEaseMultiStateListener lets us listen to multiple bloc/cubit that emits BlocEaseState
. There are so many use cases like
- Showing progress dialog when any of the cubit is still in
LoadingState
. - Showing error message if any cubit emits
FailedState
. - Showing success snackbar if only all cubits emits
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 #
BlocEaseMultiStateBuilder lets you combine different bloc/cubits that emits BlocEaseState and handle as one widget. There are so many use cases in here as well like
- Showing single loading indicator instead of one of every bloc.
- Showing single error widget instead of multiple error widgets on screen.
- Since we know how many bloc/cubits are in
LoadingState
, we can show loading progress. (Automatically handles withBlocEaseStateWidgetsProvider
-progress
field) - Showing all widget at once instead of loading separately.
- By default, we just need to pass
successBuilder
, all other states are handled by default withBlocEaseStateWidgetsProvider
.
REMEMBER: If any state is
FailedState
, it draws error widget. else if any state isInitialState
, it draws initialWidget. else if any state isLoadingState
, it drawsLoadingWidget
. Only if all states areSucceedState
, it draws success widget.
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'; part '$EventsFileName$'; typedef $BlocName$State = BlocEaseState<$SuccessType$>; 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$>; class $BlocName$Bloc extends Bloc<$BlocName$Event,$BlocName$State> { $BlocName$Bloc() : super(const $BlocName$InitialState()); $ImplementationStart$ }" description="BlocEase Four state bloc template" toReformat="false" toShortenFQNames="true">
<variable name="EventsFileName" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="BlocName" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="SuccessType" 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$>; 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$>; class $CubitName$Cubit extends Cubit<$CubitName$State> { $CubitName$Cubit() : super(const $CubitName$InitialState()); $ImplementationStart$ }" 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="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';",
"",
"part '${1:eventsFileName}';",
"",
"typedef ${2:BlocName}State = BlocEaseState<${3:SuccessType}>;",
"",
"typedef ${2}InitialState = InitialState<${3}>;",
"typedef ${2}LoadingState = LoadingState<${3}>;",
"typedef ${2}SucceedState = SucceedState<${3}>;",
"typedef ${2}FailedState = FailedState<${3}>;",
"",
"typedef ${2}BlocBuilder = BlocBuilder<${2}Bloc, ${2}State>;",
"typedef ${2}BlocListener = BlocListener<${2}Bloc, ${2}State>;",
"typedef ${2}BlocConsumer = BlocConsumer<${2}Bloc, ${2}State>;",
"",
"typedef ${2}BlocEaseBuilder = BlocEaseStateBuilder<${2}Bloc, ${3}>;",
"",
"class ${2}Bloc extends Bloc<${2}Event,${2}State> {",
"\t${2}Bloc() : super(const ${2}InitialState());",
"",
"\t${4}",
"}",
]
},
"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: ([failureMessage, 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: ([failureMessage, exception, retryCallback]) {},
succeedListener: (user) {},
initialBuilder: () {},
loadingBuilder: ([progress]) {},
failureBuilder: ([failureMessage, 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...
}
}
Accessing default widget using context. #
Sometimes, we need to access the default loading widget without using builder or we need to wrap the default loading widget with some other widget. We can access the default widgets with the help of context extensions.
context.initialStateWidget
-> Default initial state widget.context.loadingStateWidget
-> Default loading state widget.context.failedStateWidget
-> Default failed state widget.
class SomeWidget extends StatelessWidget {
const SomeWidget({super.key});
@override
Widget build(BuildContext context) {
return UserBlocEaseBuilder(
loadingBuilder: ([progress]) => ColoredBox(
color: Colors.yellow,
child: context.loadingStateWidget(progress), //<--Accessing default loading widget with 'context.loadingStateWidget'
),
succeedBuilder: (user) => SomeOtherWidget(user),
);
}
}
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 BlocEaseStateWidgetsProvider
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 BlocEaseStateWidgetsProvider(
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.