The State Pattern serves as a behavioral design pattern that helps in encapsulating various behaviors for an object according to its internal state. This allows an object to modify its behavior dynamically without relying on conditional statements, ultimately enhancing maintainability.
Unpacking the Essence of the State Pattern
As per the Design Patterns in Dart resource, the State Pattern acts as a behavioral design pattern pivotal in encapsulating diverse behaviors for an object based on its internal state. With this approach, an object can adjust its behavior dynamically without the need for conditional statements, thereby simplifying the codebase.
Essentially, each state of an object gets represented by distinct classes, serving as extensions or variations of a core state class specific to that object.
Diving Deeper into the Concept
Consider having an object like Water
, where you define a WaterState
to embody variations such as Solid
, Liquid
, and Gaseous
:
abstract class WaterState {
//
}
class Solid extends WaterState {
//
}
class Liquid extends WaterState {
//
}
class Gaseous extends WaterState {
//
}
Furthermore, the State Pattern becomes a vital component of the BLoC pattern. If you are employing the BLoC pattern, you are essentially leveraging the State Pattern.
Ideal Scenarios for Implementing the State Pattern
The State Pattern comes in handy under the following circumstances:
- When an object behaves differently based on its state.
- If the number of states is substantial, and state changes are frequent.
- When a class features a multitude of conditionals affecting its behavior related to field values.
- In cases where there is significant code duplication for similar states and transitions.
Application of the State Pattern in Real Life
Let's now explore a practical example showcasing the State Pattern in action. Consider the class below, where state definitions incorporate variables and ChangeNotifier
:
class CategoryStore extends ChangeNotifier {
List<String> categories = [];
bool isLoading = false;
String error = '';
IApiDatasource apiDatasource = ApiDatasource();
void getCategories() async {
isLoading = true;
await apiDatasource.getCategories().then((response) {
response.fold(
(left) => error = left.message,
(right) => categories = right,
);
});
isLoading = false;
notifyListeners();
}
}
The fold()
method highlighted above is associated with the Either type, an essential element in functional programming that represents a value falling under one of two specified types. Typically, Either represents a successful or failed value, much like in the example where left
signifies the error value and right
signifies the success value.
Within the aforementioned class, a function initiates loading through isLoading
, then proceeds to fetch data from an API, storing it in the categories
variable. Upon completion of loading, the function adjusts the value to false.
To streamline this code using the State Pattern, we structure it as follows:
Defining State Classes
The states and their corresponding classes are declared initially:
abstract class CategoryState {}
class CategoryInitial extends CategoryState {}
class CategoryLoading extends CategoryState {}
class CategoryLoaded extends CategoryState {
final List<String> categories;
CategoryLoaded(this.categories);
}
class CategoryError extends CategoryState {
final String message;
CategoryError(this.message);
}
Refining the Store Class
In the getCategories()
function, we eliminate the isLoading
variable and introduce the value
variable, representing the state within ValueNotifier
and adhering to the CategoryState
. Consequently, CategoryLoading
is designated to signify our state.
Subsequently, in the fold()
method, if the request flounders, value
transitions into the Error
state carrying the error message (left
). Conversely, upon a successful outcome, it adopts the Loaded
state with the data residing in the right
variable. This workflow discards the categories
and error
variables:
class CategoryStore extends ValueNotifier<CategoryState> {
CategoryStore() : super(CategoryInitial());
IApiDatasource apiDatasource = ApiDatasource();
void getCategories() async {
value = CategoryLoading();
await apiDatasource.getCategories().then((response) {
response.fold(
(left) => value = CategoryError(left.message),
(right) => value = CategoryLoaded(right),
);
});
}
}
The value
getter pertains to ValueNotifier
, housing the present state of our Store class.
Demonstrating an Instance within a Page
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// CategoryStore instance establishment
CategoryStore store = CategoryStore();
// Initialization function upon page load
@override
void initState() {
store.getCategories();
super.initState();
}
@override
void dispose() {
super.dispose();
store.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
// Observing the store
child: ValueListenableBuilder(
valueListenable: store,
builder: (context, value, child) {
// Error handling
if (value is CategoryError) {
return Center(
child: Text(
'Error loading categories: ${value.message}',
),
);
}
// Presenting loaded data
if (value is CategoryLoaded) {
return ListView.builder(
itemCount: value.categories.length,
itemBuilder: (context, index) {
final category = value.categories[index];
return Text(category);
},
);
}
// Display loading status
return const Center(child: CircularProgressIndicator());
},
),
),
);
}
}
In this manner, we have successfully implemented the State Pattern! Through this approach, we adhere to the Single Responsibility and Open/Closed Principles, and streamline the code by eliminating conditional statements that could otherwise complicate the codebase.
Wrapping Up
Your journey through this exploration of the State Pattern is greatly appreciated. While the focus was on the State Pattern itself, I refrained from delving too deeply into elements like ValueNotifier
and others utilized in the example.
Top comments (0)