Explaining the Concept of the State Pattern in Flutter
Tags: flutter, dart, beginners
Published: 2024-05-26 22:36:41



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


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g951ck8fqbb7uo6cpne0.jpeg)

Consider having an object like `Water`, where you define a `WaterState` to embody variations such as `Solid`, `Liquid`, and `Gaseous`:

```dart
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`:

```dart
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:

```dart
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:

```dart
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

```dart
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.