bloc_ease

A dart library to solve boilerplate issues with flutter_bloc by just using typedefs instead of defining state classes.

bloc_ease_template

image

image

Index

Problems This Library Addresses

  1. Repeatedly writing the same types of states for every Bloc/Cubit (Initial, Loading, Success, Failure).
  2. Overriding == and hashCode, or using the Equatable package for all states.
  3. Handling every state in the UI, even when only the success state is needed.
  4. Returning the same widget for the same kind of state across all Blocs/Cubits (e.g., ProgressIndicator for the Loading state).
  5. Managing buildWhen to avoid handling every state.
  6. Adopting poor practices such as using a single-state class instead of inheritance.
  7. 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

  1. 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> vs SuccessState<User>).
  2. 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.
  3. Provision of a builder that offers the success object in a type-safe manner, while autonomously handling other states.
  4. 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 or FailureState:

    • Calculation (SuccessState<double>(10) vs FailureState<double>(DivideByZeroException()))
  • 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 temperature SuccessState<TemperatureUnit>(Celsius())

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

image

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. image

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 current loadingState or the exSuccessState with the current successState.

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 with BlocEaseStateWidgetProvider - 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 an InitialState, the initial widget is shown. If any state is a LoadingState, the loading widget is rendered. Only if all states are SuccessState, 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(),
    );
  }
}

image

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';&#10;import 'package:flutter_bloc/flutter_bloc.dart';&#10;&#10;typedef $BlocName$State = BlocEaseState&lt;$SuccessType$&gt;;&#10;&#10;class $BlocName$Bloc extends Bloc&lt;$BlocName$Event,$BlocName$State&gt; {&#10;  $BlocName$Bloc()&#10;      : super(const $BlocName$InitialState());&#10;      &#10;  $Dependencies$&#10;      &#10;  void $FunctionName$() {&#10;    emit(const $BlocName$LoadingState());&#10;    &#10;    $ImplementationStart$&#10;  }&#10;}&#10;&#10;typedef $BlocName$InitialState = InitialState&lt;$SuccessType$&gt;;&#10;typedef $BlocName$LoadingState = LoadingState&lt;$SuccessType$&gt;;&#10;typedef $BlocName$SuccessState = SuccessState&lt;$SuccessType$&gt;;&#10;typedef $BlocName$FailureState = FailureState&lt;$SuccessType$&gt;;&#10;&#10;typedef $BlocName$BlocBuilder = BlocBuilder&lt;$BlocName$Bloc, $BlocName$State&gt;;&#10;typedef $BlocName$BlocListener = BlocListener&lt;$BlocName$Bloc, $BlocName$State&gt;;&#10;typedef $BlocName$BlocConsumer = BlocConsumer&lt;$BlocName$Bloc, $BlocName$State&gt;;&#10;&#10;typedef $BlocName$BlocEaseBuilder = BlocEaseStateBuilder&lt;$BlocName$Bloc, $SuccessType$&gt;;&#10;typedef $BlocName$BlocEaseListener = BlocEaseStateListener&lt;$BlocName$Bloc, $SuccessType$&gt;;&#10;typedef $BlocName$BlocEaseConsumer = BlocEaseStateConsumer&lt;$BlocName$Bloc, $SuccessType$&gt;;" 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';&#10;import 'package:flutter_bloc/flutter_bloc.dart';&#10;&#10;typedef $CubitName$State = BlocEaseState&lt;$SuccessType$&gt;;&#10;&#10;class $CubitName$Cubit extends Cubit&lt;$CubitName$State&gt; {&#10;  $CubitName$Cubit()&#10;      : super(const $CubitName$InitialState());&#10;      &#10;  $Dependencies$&#10;      &#10;  void $FunctionName$() {&#10;    emit(const $CubitName$LoadingState());&#10;    &#10;    $ImplementationStart$&#10;  }&#10;}&#10;&#10;typedef $CubitName$InitialState = InitialState&lt;$SuccessType$&gt;;&#10;typedef $CubitName$LoadingState = LoadingState&lt;$SuccessType$&gt;;&#10;typedef $CubitName$SuccessState = SuccessState&lt;$SuccessType$&gt;;&#10;typedef $CubitName$FailureState = FailureState&lt;$SuccessType$&gt;;&#10;&#10;typedef $CubitName$BlocBuilder = BlocBuilder&lt;$CubitName$Cubit, $CubitName$State&gt;;&#10;typedef $CubitName$BlocListener = BlocListener&lt;$CubitName$Cubit, $CubitName$State&gt;;&#10;typedef $CubitName$BlocConsumer = BlocConsumer&lt;$CubitName$Cubit, $CubitName$State&gt;;&#10;&#10;typedef $CubitName$BlocEaseBuilder = BlocEaseStateBuilder&lt;$CubitName$Cubit, $SuccessType$&gt;;&#10;typedef $CubitName$BlocEaseListener = BlocEaseStateListener&lt;$CubitName$Cubit, $SuccessType$&gt;;&#10;typedef $CubitName$BlocEaseConsumer = BlocEaseStateConsumer&lt;$CubitName$Cubit, $SuccessType$&gt;;&#10;" 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>

image

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 of BlocBuilder<UserCubit, UserState>
  • UserBlocListener instead of BlocListener<UserCubit, UserState>
  • UserBlocConsumer instead of BlocConsumer<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.

Connect with me @Bharath

image

Libraries

bloc_ease