Implementation

The following implementation (or at least the idea behind it) could be applied to pretty much every Flutter application which loads resources from any kind of external source, e.g., API. For instance, when you load resources asynchronously using HTTP and calling the REST API, usually it takes some time to finish the request. How to handle this and not “freeze” the application while the request is finished? What if some kind of error occurs during this request? A simple approach is to use animations, loading/error screens, etc. It could become cumbersome when you need to implement the same logic for different kind of resources. For this, the State design pattern could help. First of all, you clarify the states which are common for all of your resources:

Empty — there are no results;

Loading — the request to load the resources is in progress;

Loaded — resources are loaded from the external source;

Error — an error occurred while loading the resources.

For all of these states, a common state interface and context is defined which could be used in the application.

Let’s dive into the implementation details of the State design pattern and its example in Flutter!

Class diagram

The class diagram below shows the implementation of the State design pattern:

Class Diagram — Implementation of the State design pattern

IState defines a common interface for all the specific states:

nextState() — changes the current state in StateContext object to the next state;

render() — renders the UI of a specific state.

NoResultsState, ErrorState, LoadingState and LoadedState are concrete implementations of the IState interface. Each of the states defines its representational UI component via render() method, also uses a specific state (or states, if the next state is chosen from several possible options based on the context) of type IState in nextState(), which will be changed by calling the nextState() method. In addition to this, LoadedState contains a list of names, which is injected using the state’s constructor, and LoadingState uses the FakeApi to retrieve a list of randomly generated names.

StateContext saves the current state of type IState in private currentState property, defines several methods:

setState() — changes the current state;

nextState() — triggers the nextState() method on the current state;

dispose() — safely closes the stateStream stream.

The current state is exposed to the UI by using the outState stream.

StateExample widget contains the StateContext object to track and trigger state changes, also uses the NoResultsState as the initial state for the example.

IState

An interface which defines methods to be implemented by all specific state classes. Dart language does not support the interface as a class type, so we define an interface by creating an abstract class and providing a method header (name, return type, parameters) without the default implementation.

StateContext

A class which holds the current state in currentState property and exposes it to the UI via outState stream. The state context also defines a nextState() method which is used by the UI to trigger the state’s change. The current state itself is changed/set via the setState() method by providing the next state of type IState as a parameter to it.

Specific implementations of the IState interface

ErrorState implements the specific state which is used when an unhandled error occurs in API and the error widget should be shown.

LoadedState implements the specific state which is used when resources are loaded from the API without an error and the result widget should be provided to the screen.

NoResultsState implements the specific state which is used when a list of resources is loaded from the API without an error, but the list is empty. Also, this state is used initially in the StateExample widget.

LoadingState implements the specific state which is used on resources loading from the FakeApi. Also, based on the loaded result, the next state is set in nextState() method.

FakeApi

A fake API which is used to randomly generate a list of person names. The method getNames() could return a list of names or throw an Exception (error) at random. Similarly, the getRandomNames() method randomly returns a list of names or an empty list. This behaviour is implemented because of demonstration purposes to show all the possible different states in the UI.

Example

First of all, a markdown file is prepared and provided as a pattern’s description:

StateExample implements the example widget of the State design pattern. It contains the StateContext, subscribes to the current state’s stream outState and provides an appropriate UI widget by executing the state’s render() method. The current state could be changed by triggering the changeState() method (pressing the Load names button in UI).

StateExample widget is only aware of the initial state class — NoResultsState but does not know any details about other possible states, since their handling is defined in StateContext class. This allows to separate business logic from the representational code, add new states of type IState to the application without applying any changes to the UI components.

The final result of the State design pattern’s implementation looks like this:

As you can see in the example, the current state is changed by using a single Load names button, states by themselves are aware of other states and set the next state in the StateContext.

All of the code changes for the State design pattern and its example implementation could be found here.