Flutter is an improvement toolkit created by means of Google that permits developers to build applications for multiple systems, together with Android, iOS, Linux, Mac, windows, Google Fuchsia, and the internet, all from a single codebase. One massive mission in making Flutter apps is handling nation, which is the data the app maintains music of as humans use it. That’s the location where BLoC rescue us. BLoC is a manner to manage country and occasions in Flutter apps. It splits up the app’s good judgment from its consumer interface, makes less difficult for us to understand, hold, and test. the use of BLoC well can make growing apps smoother and enhance the very last product.
The BLoC pattern is what holds Flutter applications when it comes to managing states and business logic in any instances. It allows for the separation of UI from the underlying logic so that a change in state will propagate a response on the user interface without having the need to control it directly. This separation helps in increasing clarity, maintainability, and testability.
This pattern enforces great separation of business logic from UI code and is very modular and creates highly testable applications. It means that to use effectively the BLoC pattern, you must at least know the basics of reactive programming since the pattern relies on streams for handling state changes.
In Flutter, you can implement the BLoC pattern using the Flutter BLoC library. Key components include:
One of the most predominant challenges in apps is to write code that is clear, organized, maintainable, and testable. While rapid development might be needed, sometimes the burden for sustaining a codebase requires an architecture that would comfortably host scalable code, hence making it easy to add new features. Separating user interface from business logic is how to ideally realize clean and maintainable code in Flutter. The separation is provided by a BLoC pattern, so it’s easier to manage and scale your application.
While the BLoC pattern has many advantages, it’s crucial to evaluate other state management solutions in Flutter, such as Provider, Riverpod, and GetX, each with its own unique benefits. Provider is a popular choice that uses InheritedWidgets for state management, offering a straightforward and effective solution, especially for new applications. Riverpod, building on Provider’s principles, provides a more flexible and robust approach, eliminating the need for context to access providers, which enhances testability and scalability.
GetX is known for its simplicity and performance, integrating reactive state management, dependency injection, and route management into a single package. This makes it ideal for projects that require quick development cycles and minimal boilerplate code. The decision between BLoC, Provider, Riverpod, and GetX ultimately depends on your project’s specific needs, your team’s familiarity with these tools, and personal preferences, as each offers distinct advantages suited to different development scenarios.
Before starting to develop in Flutter with BLoC, the development environment has to be correctly set up. In this guide, you will go through all the necessary steps to start developing in Flutter with the BLoC library properly.
By following these steps, you’ll ensure your development environment is properly configured for efficient Flutter development
flutter create flutter_bloc
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.0.1
equatable: ^2.0.3
bloc:
http:
This folder structure is designed to organize a Flutter application in a very well-ordered and modular way, making it scalable. This separation of concerns will give maintainability and makes development easier. Here is what each directory does:
main.dart File
This main.dart file serves as the entry point for the Flutter application. It sets up the main widget, configures the app’s theme, and defines the initial route and route generation for navigation within the app.
import 'package:flutter/material.dart';
import 'package:logindemobloc/config/routes/routes.dart';
import 'package:logindemobloc/config/routes/routes_name.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
initialRoute: RoutesName.login, // Initial route
onGenerateRoute: Routes.generateRoute, // Generating routes
debugShowCheckedModeBanner: false,
);
}
}
login_event.dart File
The event file is where you define classes that represent distinct events that can occur within your application’s login functionality. These events typically encapsulate user interactions, asynchronous operations like API calls, or any action that can trigger a change in the application’s state.
abstract class LoginEvent extends Equatable {
const LoginEvent();
@override
List<Object> get props => [];
}
An Abstract Class: LoginEvent is an abstract class in Dart; therefore, it cannot exist on its own but serves like a blueprint to its subclasses.
Equatable: A utility class in Dart that extends Equatable to help with value-based equality checks. As a result, it defines props as the properties that are used for checking equality.
class EmailChanged extends LoginEvent {
const EmailChanged({required this.email});
final String email;
@override
List<Object> get props => [email];
}
EmailChanged: Represents an event where the user changes their email input.
Constructor: Takes a required email parameter.
Equatable Override: Overrides props to include only email, ensuring equality comparison considers only the email field.
PasswordChanged Event
class PasswordChanged extends LoginEvent {
const PasswordChanged({required this.password});
final String password;
@override
List<Object> get props => [password];
}
PasswordChanged: Represents an event where the user changes their password input.
Constructor: Takes a required password parameter.
Equatable Override: Overrides props to include only password.
LoginApi Event
class LoginApi extends LoginEvent {}
LoginApi: Represents an event where the user initiates a login via an API call.
No Additional Data: This event does not require additional parameters beyond its type.
Inherits props from LoginEvent: Since it doesn’t have additional properties, it inherits the props definition from LoginEvent, which returns an empty list by default.
Full Code:
part of 'login_bloc.dart';
abstract class LoginEvent extends Equatable {
const LoginEvent();
@override
List<Object> get props => [];
}
class EmailChanged extends LoginEvent {
const EmailChanged({required this.email});
final String email;
@override
List<Object> get props => [email];
}
class PasswordChanged extends LoginEvent {
const PasswordChanged({required this.password});
final String password;
@override
List<Object> get props => [password];
}
class LoginApi extends LoginEvent {}
login_state.dart File
The LoginState class serves as a blueprint for managing and representing the state of the login screen. It encapsulates all relevant data and status indicators necessary for rendering the UI based on different user interactions and asynchronous operations.
part of 'login_bloc.dart';
enum LoginStatus { initial, loading, success, error }
class LoginState extends Equatable {
const LoginState({this.email = '', this.password = '', this.message = '', this.loginStatus = LoginStatus.initial});
final String email;
final String password;
final String message;
final LoginStatus loginStatus;
LoginState copyWith({
String? email,
String? password,
String? message,
LoginStatus? loginStatus,
}) {
return LoginState(
email: email ?? this.email,
password: password ?? this.password,
message: message ?? this.message,
loginStatus: loginStatus ?? this.loginStatus,
);
}
@override
List<Object> get props => [email, password, message, loginStatus];
}
login_bloc.dart File
Effectively managing intricate state and business logic is paramount to crafting robust Flutter applications. The BLoC architecture offers a structured approach to decoupling UI components from core application logic. By encapsulating business rules within dedicated components, BLoC enhances code clarity, adaptability, and testability. Let’s explore how to implement a LoginBloc to masterfully handle state and logic within a login screen.
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:logindemobloc/data/repositories/login/login_services.dart';
part 'login_event.dart';
part 'login_state.dart';
Imports: Essential packages (bloc and equatable) and a custom LoginServices class for handling login operations.
Part Directives: Includes login_event.dart and login_state.dart, which define events and states used by LoginBloc.
LoginBloc Class Definition
class LoginBloc extends Bloc<LoginEvent, LoginState> {
LoginServices loginServices = LoginServices();
LoginBloc() : super(const LoginState()) {
on<EmailChanged>(_onEmailChanged);
on<PasswordChanged>(_onPasswordChanged);
on<LoginApi>(_loginApi);
}
// Event handlers
void _onEmailChanged(EmailChanged event, Emitter<LoginState> emit) {
emit(
state.copyWith(
email: event.email,
),
);
}
void _onPasswordChanged(PasswordChanged event, Emitter<LoginState> emit) {
emit(
state.copyWith(
password: event.password,
),
);
}
void _loginApi(LoginApi event, Emitter<LoginState> emit) async {
emit(
state.copyWith(
loginStatus: LoginStatus.loading,
),
);
bool isLogin = await loginServices.loginServices(
email: state.email, password: state.password);
try {
if (isLogin) {
emit(
state.copyWith(
loginStatus: LoginStatus.success,
message: 'Login successful',
),
);
} else {
emit(
state.copyWith(
loginStatus: LoginStatus.error,
message: 'Login failed',
),
);
}
} catch (e) {
emit(
state.copyWith(
loginStatus: LoginStatus.error,
message: e.toString(),
),
);
}
}
}
Initialization:
Event Handlers:
Login_screen.dart File
It uses a StatefulWidget; thus, it updates dynamically in response to user interaction or state change events. This file integrates BLoC with Flutter UI using the flutter_bloc package. This gives clear separation between the UI and the business logic for this file.
LoginScreen Widget
class LoginScreen extends StatefulWidget {
const LoginScreen({Key? key}) : super(key: key);
@override
State<LoginScreen> createState() => _LoginScreenState();
}
The LoginScreen class extends StatefulWidget, meaning it has a mutable state managed by _LoginScreenState.
_LoginScreenState Class
class _LoginScreenState extends State<LoginScreen> {
late LoginBloc _loginBlocs;
final emailFocusNode = FocusNode();
final passwordFocusNode = FocusNode();
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
_loginBlocs = LoginBloc();
}
@override
void dispose() {
_loginBlocs.close();
super.dispose();
}
State Variables:
Lifecycle Methods:
build Method
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login'),
),
body: BlocProvider(
create: (_) => _loginBlocs,
child: Padding(
padding: const EdgeInsets.all(20),
child: Form(
key: _formKey,
child: BlocListener<LoginBloc, LoginState>(
listenWhen: (previous, current) =>
current.loginStatus != previous.loginStatus,
listener: (context, state) {
if (state.loginStatus == LoginStatus.error) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(content: Text(state.message.toString())),
);
}
if (state.loginStatus == LoginStatus.success) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
const SnackBar(content: Text('Login successful')),
);
Navigator.pushNamedAndRemoveUntil(
context, RoutesName.home, (route) => false);
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
BlocBuilder<LoginBloc, LoginState>(
buildWhen: (current, previous) =>
current.email != previous.email,
builder: (context, state) {
return TextFormField(
keyboardType: TextInputType.emailAddress,
focusNode: emailFocusNode,
decoration: const InputDecoration(
hintText: 'Email', border: OutlineInputBorder()),
onChanged: (value) {
context
.read<LoginBloc>()
.add(EmailChanged(email: value));
},
validator: (value) {
if (value!.isEmpty) {
return 'Enter email';
}
return null;
},
onFieldSubmitted: (value) {},
);
}),
const SizedBox(
height: 20,
),
BlocBuilder<LoginBloc, LoginState>(
buildWhen: (current, previous) =>
current.password != previous.password,
builder: (context, state) {
return TextFormField(
keyboardType: TextInputType.text,
focusNode: passwordFocusNode,
decoration: const InputDecoration(
hintText: 'Password',
border: OutlineInputBorder()),
onChanged: (value) {
context
.read<LoginBloc>()
.add(PasswordChanged(password: value));
},
validator: (value) {
if (value!.isEmpty) {
return 'Enter password';
}
return null;
},
onFieldSubmitted: (value) {},
);
}),
const SizedBox(
height: 50,
),
BlocBuilder<LoginBloc, LoginState>(
buildWhen: (current, previous) =>
current.loginStatus != previous.loginStatus,
builder: (context, state) {
return ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
context.read<LoginBloc>().add(LoginApi());
}
if (state.loginStatus == LoginStatus.success) {
Navigator.pushNamedAndRemoveUntil(
context, RoutesName.home, (route) => false);
}
},
child: state.loginStatus == LoginStatus.loading
? CircularProgressIndicator()
: const Text('Login'));
}),
],
),
),
),
),
),
);
}
Scaffold: Provides the structure of the screen with an AppBar and a body containing the form.
BlocProvider: Injects the LoginBloc into the widget tree.
Padding and Form: The form is wrapped in padding and managed by _formKey to handle validation.
BlocListener: Listens for changes in the LoginState. Based on loginStatus, it shows snackbars for error and success messages and navigates to the home screen on successful login.
Column: Organizes the form fields and buttons in a vertical layout.
BlocBuilder: Builds widgets based on the LoginState.
Login_services.dart File
class LoginServices {
Future<bool> loginServices({String? email, String? password}) async {
Map data = {'email': email, 'password': password};
try {
final response =
await http.post(Uri.parse('https://reqres.in/api/login'), body: data);
if (response.statusCode == 200) {
return true;
} else {
return false;
}
} on SocketException {
await Future.delayed(const Duration(milliseconds: 1800));
throw Exception('No Internet Connection');
}
}
}
Routes_name.dart File
The primary purpose of the RoutesName class is to centralize and provide a single source of truth for route names or identifiers used throughout the application. In Flutter, routes are identified by unique strings, and using constants like those in RoutesName helps avoid hard-coding strings directly in navigation operations, improving code readability and maintainability.
class RoutesName {
//accounts routes name
static const String login = 'login_screen' ;
//home screen routes name
static const String home = 'home_screen' ;
}
import 'package:flutter/material.dart';
import 'package:logindemobloc/config/routes/routes_name.dart';
import 'package:logindemobloc/features/homeScreen/screen/homescreen.dart';
import 'package:logindemobloc/features/login/screen/login_screen.dart';
class Routes {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case RoutesName.home:
return MaterialPageRoute(builder: (BuildContext context) => const HomeScreen());
case RoutesName.login:
return MaterialPageRoute(builder: (BuildContext context) => const LoginScreen());
default:
return MaterialPageRoute(builder: (_) {
return const Scaffold(
body: Center(
child: Text('No route defined'),
),
);
});
}
}
}
The BLoC pattern emerges as a cornerstone for effective state management in Flutter applications. By enforcing a strict separation between business logic and UI, BLoC fosters highly maintainable, testable, and scalable codebases. This architectural approach significantly accelerates development cycles, resulting in robust and resilient applications within the Flutter ecosystem.
Answer: Handling complex interactions involves planning how BLoCs communicate. You can use BlocProvider to connect one BLoC with another when necessary. Another method is to use streams to keep track of changes across BLoCs and coordinate actions accordingly.
Answer: Yes, you can combine BLoC with other tools like Provider or Riverpod. For example, Provider helps manage how BLoCs are accessed and used throughout your app, making it easier to inject dependencies and enhance flexibility.
Answer: Form validation in BLoC involves emitting state changes based on user input and validation logic. You can define events for different form actions (like field changes or form submission) and states to represent valid, invalid, or error states. Use streams to propagate validation results to the UI for real-time feedback.
Diving deep into SwiftUI This blog post drops us into…
Corporate efficiency and customization are vital in today's fast-paced world,…
Flutter Codemagic CI/CD makes your Flutter app build, test, and…
This website uses cookies.