Business Logic Component (BLoC) Pattern
Shopping Cart UI Design using Flutter Bloc: BLoC is popular in the Flutter community because of its separation of concerns, responsiveness, testability and scalability. However, it may require more boilerplate code than other state management approaches and has a steeper learning curve.
There are several core concepts to understand when using BLoC in Flutter:
- Events: events signify user activities or other actions that will alter the application’s state. Events are typically represent as simple data classes.
- Bloc: a Bloc is a class that takes in events, processes them, and produces a new state. It is in charge of controlling the application’s state and responding to user input.
- State: state represents the current state of the application. It is typically represents as an immutable data class.
- Stream: a stream is a collection of asynchronous events that may be monitors for modifications. In the context of BLoC, Streams are used in BLoC to describe the application’s state at any given time.
- Sink: a Sink is a Stream controller that can uses to send events to a stream. In the context of BLoC, a Sink is uses to send events to the Bloc for processing.
- StreamController: StreamController is use to construct and manage streams. In the context of BLoC, a StreamController is to manage the stream(s) of events that are sent to the Bloc.
- BlocBuilder: BlocBuilder is a widget provided by the flutter_bloc package that helps to connect the Bloc to the user interface. It listens to changes in the state of the Bloc and rebuilds the UI accordingly.
- BlocProvider: The flutter_bloc package has a widget called BlocProvider that adds a Bloc to the widget tree. It ensures that the Bloc is created only once and is accessible to all the widgets in the subtree.
Core concepts:
These are some of the core concepts which is essential to understanding the BLoC pattern in Flutter. By understanding these concepts, you will create well-architected Flutter applications that are easy to maintain and test.
BLoC refers to the Business Logic Component pattern, which is a state management pattern while bloc (lowercase) is a term often uses to refer to an instance of the Bloc class which implements the BLoC pattern.
So while the two terms relate, they refer to different concepts – BLoC refers to a design pattern, while bloc refers to an instance of a class that implements that pattern. We are going to be using both words so it’s good to recognize the difference.
Create an Shopping cart application using the BLoC pattern
Shopping Cart using Flutter Bloc:
We will be creating a simple application of a container that changes its color from red to blue using bloc, events, and states. To use the Bloc pattern for state management, we must add the flutter_bloc package to our project’s dependencies.
dependencies:
flutter_bloc: ^8.1.3
After adding the package to our project, we are going to create a folder inside our lib folder calls bloc, this folder is going to hold our home_bloc.dart file, home_ event.dart file and home_state.dart file, you will create those three files inside the bloc folder.
Once we have success in creating the folder and files, we are going to proceed to define the events in the Home_event.dart file.
In this part of the code, we define a set of classes that represent events in a BLoC.
part of 'home_bloc.dart';
@immutable
sealed class HomeEvent {}
class HomeInitialEvent extends HomeEvent {}
class HomeProductWishButtonClickEvent extends HomeEvent {}
class HomeProductCartButtonClickEvent extends HomeEvent {}
class HomeProductWishButtonNavigateEvent extends HomeEvent {}
class HomeCartWishButtonNavigateEvent extends HomeEvent {}
home_bloc.dart
:
The first line part of home_bloc.dart'
; indicates that the code is part of a larger file home_bloc.dart which is going to contain the implementation of the BLoC.
The @immutable
annotation is to indicate that instances of the classes defines below are immutable and cannot be change once creates. This helps to ensure that the state of the application is not accidentally modifies.
The abstract class HomeEvent {}
defines an abstract class HomeEvent
that will be use as a base class for all the events in the BLoC. This class does not contain any implementation and cannot be instantiated directly.
The InitialEvent
class represents the initial event that is send to the BLoC when it is first creates. It is typical to initialize the state of the application.
Let us define the states that the Bloc will handle.
part of 'home_bloc.dart';
@immutable
sealed class HomeState {}
abstract class HomeActionState extends HomeState {}
final class HomeInitial extends HomeState {}
class HomeLoadingState extends HomeState {}
class HomeLoadedSuccessState extends HomeState {
final List<ProductDataModel> products;
HomeLoadedSuccessState({required this.products});
}
class HomeErrorState extends HomeState {}
class HomeNavigateWishActionState extends HomeActionState {}
class HomeNavigateCartActionState extends HomeActionState {}
These classes define the different states the bloc can in and help to ensure that the state of the application is updates in a predictable and controlling way.
We then define a Bloc class that handles the events and updates the state of the application.
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:bloc1/data/ecommerce.dart';
import 'package:bloc1/features/home/models/home_product_model.dart';
import 'package:meta/meta.dart';
part 'home_event.dart';
part 'home_state.dart';
class HomeBloc extends Bloc<HomeEvent, HomeState> {
HomeBloc() : super(HomeInitial()) {
// on<HomeEvent>((event, emit) {
// // TODO: implement event handler
// });
on<HomeInitialEvent>(homeInitialEvent);
on<HomeProductCartButtonClickEvent>(homeProductCartButtonClickEvent);
on<HomeProductWishButtonClickEvent>(homeProductWishButtonClickEvent);
on<HomeProductWishButtonNavigateEvent>(homeProductWishButtonNavigateEvent);
on<HomeCartWishButtonNavigateEvent>(homeCartWishButtonNavigateEvent);
}
FutureOr<void> homeInitialEvent(
HomeInitialEvent event, Emitter<HomeState> emit) async {
emit(HomeLoadingState());
await Future.delayed(Duration(seconds: 3));
emit(HomeLoadedSuccessState(
products: Ecommerce.ecommerceProducts
.map((e) => ProductDataModel(
id: e['id'],
name: e['name'],
category: e['category'],
price: e['price'],
image: e['image']))
.toList()));
}
FutureOr<void> homeProductCartButtonClickEvent(
HomeProductCartButtonClickEvent event, Emitter<HomeState> emit) {
print('Cart Clicked');
}
FutureOr<void> homeProductWishButtonClickEvent(
HomeProductWishButtonClickEvent event, Emitter<HomeState> emit) {
print('Wishlist Clicked');
}
FutureOr<void> homeProductWishButtonNavigateEvent(
HomeProductWishButtonNavigateEvent event, Emitter<HomeState> emit) {
print('Wish Navigate Clicked');
emit(HomeNavigateWishActionState());
}
FutureOr<void> homeCartWishButtonNavigateEvent(
HomeCartWishButtonNavigateEvent event, Emitter<HomeState> emit) {
print('Cart Navigate Clicked');
emit(HomeNavigateCartActionState());
}
}
import 'package:bloc1/features/home/ui/home.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primaryColor: Colors.teal),
debugShowCheckedModeBanner: false,
home: Home(),
);
}
}
Home.dart: Shopping Cart using Flutter Bloc
import 'package:bloc1/features/cart/ui/cart.dart';
import 'package:bloc1/features/home/bloc/home_bloc.dart';
import 'package:bloc1/features/home/ui/product_tile.dart';
import 'package:bloc1/features/wish/ui/wish.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class Home extends StatefulWidget {
Home({Key? key}) : super(key: key);
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
@override
void initState() {
// TODO: implement initState
super.initState();
homeBloc.add(HomeInitialEvent());
}
final HomeBloc homeBloc = HomeBloc();
@override
Widget build(BuildContext context) {
return BlocConsumer<HomeBloc, HomeState>(
bloc: homeBloc,
listenWhen: ((previous, current) => current is HomeActionState),
buildWhen: ((previous, current) => current is! HomeActionState),
listener: (context, state) {
if (state is HomeNavigateCartActionState) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => Cart()));
} else if (state is HomeNavigateWishActionState) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => Wish()));
}
// TODO: implement listener
},
builder: (context, state) {
switch (state.runtimeType) {
case HomeLoadingState:
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
case HomeLoadedSuccessState:
final successState = state as HomeLoadedSuccessState;
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.teal,
title: Text("Elite Ecommerce"),
actions: [
IconButton(
onPressed: () {
homeBloc.add(HomeProductWishButtonNavigateEvent());
},
icon: Icon(Icons.favorite_border_outlined)),
IconButton(
onPressed: () {
homeBloc.add(HomeCartWishButtonNavigateEvent());
},
icon: Icon(Icons.shopping_cart_outlined))
],
),
body: ListView.builder(
itemCount: successState.products.length,
itemBuilder: (context, index) {
return ProductTile(
productDataModel: successState.products[index]);
}),
);
case HomeErrorState:
return Scaffold(
body: Center(
child: Text("Error"),
),
);
default:
return SizedBox();
}
},
);
}
}
Product_Tile.dart
The BlocConsumer
widget has two builder functions: a builder function and a listener function.
The builder function is a function responsible for creating the user interface (UI) based on the current state of the BLoC. On the other hand, the listener function is called whenever a new state emits by the BLoC, allowing the UI to be updated accordingly.
To put it simply, the builder function builds the initial UI, while the listener function is responsible for updating the UI whenever there is a change in the state of the BLoC.
This helps keep the UI in sync with the underlying data and provides a clear separation of concerns between the presentation and business logic.
import 'package:bloc1/features/home/models/home_product_model.dart';
import 'package:flutter/material.dart';
class ProductTile extends StatelessWidget {
final ProductDataModel productDataModel;
const ProductTile({Key? key, required this.productDataModel})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(10),
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 200,
width: double.maxFinite,
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(10),
image: DecorationImage(
fit: BoxFit.fill,
image: NetworkImage(
productDataModel.image,
))),
),
SizedBox(
height: 5,
),
Text(
productDataModel.name,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(
height: 10,
),
Text(
productDataModel.category,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w400),
),
SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'\$' + productDataModel.price.toString(),
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
),
Row(
children: [
IconButton(
onPressed: () {
// homeBloc.add(HomeProductWishButtonNavigateEvent());
},
icon: Icon(Icons.favorite_border_outlined)),
IconButton(
onPressed: () {
// homeBloc.add(HomeCartWishButtonNavigateEvent());
},
icon: Icon(Icons.shopping_cart_outlined))
],
)
],
)
],
),
);
}
}
Shopping Cart using Flutter Bloc
For More: To know about FlutterBloc State Management
Leave a Reply