DEV Community

Cover image for Explaining the Concept of the State Pattern in Flutter
Anurika Joy
Anurika Joy

Posted on

Explaining the Concept of the State Pattern in Flutter

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

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 {
  //
}


Enter fullscreen mode Exit fullscreen mode

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();
  }
}


Enter fullscreen mode Exit fullscreen mode

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);
}


Enter fullscreen mode Exit fullscreen mode

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),
      );
    });
  }
}


Enter fullscreen mode Exit fullscreen mode

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());
          },
        ),
      ),
    );
  }
}


Enter fullscreen mode Exit fullscreen mode

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)