State Graph Architecture

Experimental: This architecture is currently pre-release. Its a first look at a new way to build your Flutter apps, given the unique way Flutter manages its user interface. Constructive comments are welcome. This architecture is not currently under testing in any production application. This architecture was inspired by What even is “navigation”? by Matt Carroll.

State Graph Architecture is all about designing your app around state and not the UI. This is done by creating two different (loosely defined as) graphs. A compile-time graph, that manages all your logic and how to handle state. And a runtime graph that holds the current state your application is in.

The major rethinking of this architecture, comes in throwing out the concept of pages and views. Your app has states, not pages. There is no navigation, only state transition.

State Nodes

The first thing we need to do, is completely throw out the concept of pages. Using the very basic example of logging into the app, we create a node to do this, with pre-defined states that are possible. You pre-define a state, pass it to a node to render, then receive the appropriate widget.

In this example the Login node will have 2 possible states it can handle. Default, and and Error state.

class DefaultLoginState implements IState {
  @override
  Branch branch = Branch.login;
}

class LoginErrorState extends LoginState {
  String loginErrorMessage = "There was an error";
}

A node is now created to handle the UI

class LoginNode {
  static Widget render(IState state) {
    if (state is LoginErrorState)
      return new Center(
          child:
              Text(state.loginErrorMessage, textDirection: TextDirection.ltr));
    else
      return new Center(
          child: FlatButton(
        child: Text(
          'Login',
          textDirection: TextDirection.ltr,
          style: TextStyle(color: Colors.white),
        ),
        onPressed: () => loginButtonPressed(state),
      ));
  }

  static loginButtonPressed(IState state) {
    // Do some logic
    StateGraph.instance.apply(new LoginErrorState());
  }
}

The node contains a render function that returns a Widget based upon the state. How you implement this is entirely up to you. You could easily add in Rx here to output to the State Graph as required.

State Graph

The State Graph, is the runtime graph. It holds each state as you transition. As demonstrated in the above example, when the loginButtonPressed method is called, it will call the StateGraph to apply a new state. This will add the state to the state graph.

Render Graph

The Render Graph is a static compile time graph. It is where you place all your logic for state transition. To keep it concise for this demonstration, it is all included in the one file.

class RenderGraph
{
  static RenderGraph _instance = new RenderGraph();
  static RenderGraph instance = _instance;

  Widget build(IState state)
  {
    if (state.branch == Branch.login)
      return LoginNode.render(state);

    return _unknownState();
  }

  Widget _unknownState()
  {
    return new Center(
        child: Text('Entered an unknown state',
            textDirection: TextDirection.ltr));
  }
}

The graph receives the state, which passes it to the relevant node for rendering. This class would contain all required state transition logic. Create a TransitionNode to separate out more complex transitions and sharing logic.

Attached States

Note: This is not implemented, but a concept to look at further.

States never just involve the UI, they involve hidden elements such as access tokens for REST API access or user details. These states do not change at the same time the UI state changes, or are not relevant when back tracking through states. Examples include the users name will always remain the same, but an access token will not. Considering this, I think a further enhancement to the state graph would include attaching a reference to a state object, that maintains its own graph.

Benefits

  1. The Render Graph contains node transition logic, instead of the nodes. I actually spoke about the benefits of getting the navigation out of pages in Workflow Controller for Xamarin.
  2. The app state is kept in the State Graph, which can be reversed as needed. Expanding upon the State Graph could enable the easy creation branches you can move up and down, as required.
  3. There is less chance of unexpected state. Each possible state change is defined. Instead of creating an object that responds to any number of permutations of state, you pass through only states you have specifically built to handle.
  4. Page states do not need to be handled. The UI is driven from the state, a page state does not dictate an application state. In other words, no OnViewAppeared or OnDisappeared level events are needed.

FAQ

Is there a GitHub repo?

Does Hot Reload still work?

  • Yes. I can change the current states UI and it will perform a hot reload.

Wouldn’t functional programming be great here?

  • Functional programming would be highly beneficial for Flutter. Dart however, is an object orientated language.

This seems tightly coupled, can you make it looser?

  • Yes, but I actually oppose loose coupling in most circumstances in app level development. It is unlikely that you will need to swap out functionality, nor keep anything too isolated. If something needs removing or changing, I would rather compile time errors, than harder to find runtime errors.

Is this better than other architectures?

  • I have no idea, because it could turn out to be completely useless, the next biggest thing, or somewhere in between.

Are you going to further enhance this architecture?

  • It depends on the feedback. If it gains positive traction, then yes.

Mobile Developer, Microsoft & Xamarin MVP

Share on
© 2018 Adam Pedley