flosha 1.0.0 copy "flosha: ^1.0.0" to clipboard
flosha: ^1.0.0 copied to clipboard

Boilerplate for handling state with BLoC pattern in a smooth way

Flosha #

Boilerplate for handling state with BLoC/Cubit pattern in a smooth way

Features #

  • ✅ Easy state-handling
  • ✅ Bult-in state-handling widget
  • ✅ Form state-handling support
  • ✅ Pull to Refresh support
  • ✅ Pagination support
  • ✅ State changes logging
  • ✅ Cubit wrapper
  • ❌ Bloc wrapper
  • ❌ Local cache support

Get Started #

To install flosha, add these following codes into your pubspec.yaml file:

dependencies:
  ...
  flosha: ^1.0.0

The Logics #

BaseObjectLogic #

BaseObjectLogic is a class that handle business logic and state manipulation with data type of T. It's like a 'controller' class that have several functions to load data from data source and handle pull-to-refresh functionality.

It has 4 functions to manipulate state:

  • loading()
    To invoke a loading state
  • empty()
    To invoke an empty state. It will set data field inside state to [] or empty list. Will display default empty widget if you use StateContainer or StateConsumer.
  • success(data)
    To invoke a success state. It takes only data as parameters, whereas data is an object/data loaded from data source and will be populated to the UI.
  • error(errorTitle, errorMessage)
    To invoke an error state. It takes two parameters, errorTitle is a title you want to display on the default error widget; and errorMessage is a message you want to display on the default error widget.
class ProductDetailLogic extends BaseObjectLogic<Product> {
  final int? id;

  ProductDetailLogic(this.id) : super(const BaseObjectState());

  @override
  void onInit() {
    super.onInit();

    loadData();
  }

  @override
  void refreshData() {
    loadData();
  }

  @override
  Future<void> loadData() async {
    try {
      loading();

      final result = await DummyApiClient.instance().getSingleProduct(id ?? 0);

      success(result);
    } catch (e) {
      error(errorTitle: "Something went wrong", errorMessage: e.toString());
    }
  }
}

BaseListLogic #

BaseListLogic is a class that handle business logic and state manipulation with data type of List<T>. It's like a 'controller' class that have several functions to load data from data source, handle pull-to-refresh functionality, and pagination.

It has 4 functions to manipulate state:

  • loading()
    To invoke a loading state
  • empty()
    To invoke an empty state. It will set data field inside state to [] or empty list. Will display default empty widget if you use StateContainer or StateConsumer.
  • success(data, page)
    To invoke a success state. It takes two parameters, data is a list of data loaded from data source and will be populated to the UI; and page to indicate current position of these set of data on pagination manners.
  • error(errorTitle, errorMessage)
    To invoke an error state. It takes two parameters, errorTitle is a title you want to display on the default error widget; and errorMessage is a message you want to display on the default error widget.
class ProductsLogic extends BaseListLogic<Product> {
  ProductsLogic() : super(const BaseListState());

  @override
  void onInit() {
    super.onInit();

    loadData();
  }

  @override
  void loadNextData() {
    loadData(page: state.page + 1);
  }

  @override
  void refreshData() {
    loadData();
  }

  @override
  Future<void> loadData({int page = 1}) async {
    try {
      loading();

      final result = await DummyApiClient.instance().getAllProducsts(
        page: page,
        pageSize: pageSize,
      );

      success(data: result, page: page);
    } catch (e) {
      error(errorTitle: "Error occured", errorMessage: e.toString());
    }
  }
}

BaseFormLogic #

BaseObjectLogic is a class that handle business logic and state manipulation with data type of T. It's like a 'controller' class that have several functions to load data from data source and handle pull-to-refresh functionality.

It has 3 functions to manipulate state:

  • loading()
    To invoke a loading state
  • success(data)
    To invoke a success state. It takes only data as parameters, whereas data is an object/data loaded from data source and will be populated to the UI.
  • error(errorTitle, errorMessage)
    To invoke an error state. It takes two parameters, errorTitle is a title you want to display on the default error widget; and errorMessage is a message you want to display on the default error widget.

Also, the dataType getter defines the data type of the response, wether it's List<T> or just an object of T.

class CreateProductLogic extends BaseFormLogic<Product> {
  CreateProductLogic() : super(const BaseFormState());

  @override
  BaseLogicDataType get dataType => BaseLogicDataType.object;

  Future<void> createProduct() async {
    if (saveAndValidateForm) {
      try {
        loading();

        Future.delayed(const Duration(seconds: 2), () async {
          final result = await DummyApiClient.instance().createProduct(values);

          log("Result: ${result.toJson()}");

          success(data: result);
        });
      } catch (e) {
        log("Error: ${e.toString()}");
        error(errorTitle: "Something went wrong", errorMessage: e.toString());
      }
    }
  }
}

The Widgets #

💡 Dont forget to Wrap these widget within BlocProvider and Builder at the top of widget tree

return BlocProvider(
    create: (_) => ProductsLogic(),
    child: Builder(
        builder: (builderCtx) {
            final logic = builderCtx.read<ProductsLogic>();

            return Scaffold(
                floatingActionButton: FloatingActionButton(
                    child: const Icon(Icons.add),onPressed: () {
                        Navigator.of(context).pushNamed(CreateProductPage.route);
                    },
                ),
                body: SafeArea(
                    child: StateContainer<BaseListState<Product>, Product>(
                        logic: logic,
                        successWidget: SuccessWidget(),
                    ),
                ),
            );
        },
      ),
    );

StateContainer #

StateContainer is a Widget that handle state changes and rebuilding UI based on certain state. It provides default UI for loading, empty, and error state, while you have to provide a Widget that will displayed when state is success by yourself.

// To handle List data type
StateContainer<BaseListState<Product>, Product>(
    // Logic class that manage state and business logic
    logic: logic,
    successWidget: SuccessWidget(),
)

or

// To handle Object data type
StateContainer<BaseObjectState<Product>, Product>(
    // Logic class that manage state and business logic
    logic: logic,
    successWidget: SuccessWidget(),
)

StateListener #

StateListener is a Widget that provides a listener, which reacts on state changes. It's use BlocListener to listen state changes, and then do something, such as showing loading overlay or Snackbar to inform user about the process.

StateLiestener<ProductsLogic, BaseListState<Product>, Product>(
    // Logic class that manage state and business logic
    logic: logic,
    child: child,
    onListen: (result){},
)

StateConsumer #

StateConsumer is combination between StateContainer and StateListener, which rebuild UI and execute block of code in every state changes. It combines BlocListener alongside BlocBuilder to handle state changes.

StateConsumer<ProductsLogic, BaseListState<Product>, Product>(
    logic: logic,
    onListen: (result) {
        // Execute code when the state changes
    }
    successWidget: SuccessWidget(),
)

StateForm #

StateForm is a Widget that handle state changes and reflecting it in the UI when using Forms. It's mainly use BlocListener to listen the state changes and then do something, such as showing loading overlay or Snackbar to inform user about the Form submission's status.

StateForm(
    logic: logic,
    onLoading: () {
        // Do something when state is loading
        // Show loading overlay, for example
    },
    onListen: (result) {
        // Do something when any state is changed (except loading). Show Snackbar, for example
        // Result has type of Either<L, R>, which you can use fold() to handle the error and success data
    },
    onSubmit: () {
        // Do something when form is submitted
        // Triggered when default Submit button is pressed
    },
    child: SomeeWidget(),
)

ScrollableListWidget #

ScrollableListWidget is a custom Widget that provide work-around to use pagination on a list with some additional widget. This widget simplify the use of SmartRefresher as the direct parent of ListView widget or other Scrollable widget.

StateContainer<BaseListState<Product>, Product>(
    logic: logic,
    successWidget: ScrollableListWidget(
    prefixWidgets: const [
        Text(
            "Products",
            style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
            ),
        ),
    ],
    datas: logic.data,
    padding: const EdgeInsets.all(16),
    separator: (index) => const SizedBox(height: 16),
    listItem: (index) => ProductListItem(
        data: logic.data[index],
        onPressed: (value) {
            Navigator.of(context).pushNamed(
                ProductDetailPage.route,
                arguments: logic.data[index].id,
                );
            },
        ),
    ),
)
2
likes
50
points
15
downloads

Publisher

unverified uploader

Weekly Downloads

Boilerplate for handling state with BLoC pattern in a smooth way

Repository (GitHub)
View/report issues

License

BSD-3-Clause (license)

Dependencies

dartz, equatable, flutter, flutter_bloc, flutter_form_builder, form_builder_validators, freezed_annotation, logger, pull_to_refresh

More

Packages that depend on flosha