Concurrent State Management

Side effects appear ever present in many state management approaches taken. MVU (Model – View – Update) was the pattern that I thought showed immense promise when I initially looked at it. As I delved in, the issue struck me. I saw a diagram with an arrow pointing out to the side, and a circle that said ‘side effects’.

Mobile apps don’t work on a page by page basis. While pages are usually the main component of an app, they are really a mashup of services, flying messages around as they see fit. You don’t know when a low battery, session timeout, network outage or user is going to interact or in what order, and managing this flow of events, ends up causing many state issues. Managing state is easy, but I was trying to manage concurrency.

State, Update and Platform (SUP)

A minor variation on MVU, gives you a State (a Model), which calls an Update function, that produces a Platform message (or in this case a View/Widget) that is sends to the Platform. Messages from the platform can come back via Callbacks into an inbound queue, to run through the Update function and trigger another update to the platform.

User Interface as a Service

Based on this, I created a reusable base class, and as a result can define a View (or Platform Model) as so.

class LoginView extends View<_LoginModel>{
  LoginView({Key key})
      : super(
            initial:
                _LoginModel(username: '', isLoggingIn: false),
            update: update,
            view: view) {
    logic.flow(this);
  }
}

@immutable
class _LoginModel {
  const _LoginModel({this.username, this.isLoggingIn});
  final String username;
  final bool isLoggingIn;
}

_LoginModel update(BuildContext context, Message msg, _LoginModel model) {  
  if (msg is LoginMessage)
    return _LoginModel(username: msg.username, isLoggingIn: true);

  if (msg is LoginResult)
    if (msg.success)
      return _LoginModel(username: '', isLoggingIn: false);

  return model;
}

Widget view(_LoginModel model, Send send) {
  var usernameController = new TextEditingController(text: model.username);
  var passwordController = new TextEditingController(text: '');

  return SafeArea(
      child: Scaffold(
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('Username:'),
          TextField(controller: usernameController),
          Text('Password:'),
          TextField(
            controller: passwordController,
            obscureText: true,
          ),
          model.isLoggingIn
              ? new CircularProgressIndicator()
              : FlatButton(
                  child: Text('Login'),
                  onPressed: () => send(LoginMessage(
                      username: usernameController.text,
                      password: passwordController.text)),
                )
        ],
      ),
    ),
  ));
}

@immutable
class LoginMessage implements Message {
  const LoginMessage({this.username, this.password});
  final String username;
  final String password;
}

@immutable
class LoginResult implements Message {
  const LoginResult({this.success});
  final bool success;
}

Because the update function is synchronously called, you could leave out the immutability if desired, since the update function is the only place where the State (Model) will ever be updated. However, keeping the Model immutable does add protection from developers mistakenly modifying the Model elsewhere.

API as a Service

To keep an identical pattern in all services, this is how an API service may be created.

final Api instance = Api();

class Api extends Base<_ApiModel, Request> {
  Api()
      : super(
            initial: _ApiModel(null),
            update: state,
            platform: platform,
            connector: connector);
}

@immutable
class _ApiModel {
  const _ApiModel(this.request);
  final http.Request request;
  final String baseUrl = 'https://sample.com';
  // Could put Auth credential e.g. AccessToken in here
}

_ApiModel state(_ApiModel model, Message message) {
  if (message is ApiPostMessage)
    return _ApiModel(
        http.Request('POST', Uri.parse(model.baseUrl + message.url)));

  return _ApiModel(null);
}

Request platform(_ApiModel model, Send send) {
  if (model.request == null) return null;

  // Build request
  return Request(model.request, (response) {
    // Ideally some kind of message hook to determine the right message response
    send(ApiResponseMessage(status: response.statusCode));
  });
}

void connector(Request request) {
  if (request == null) return;

  // Sample delay
  Future.delayed(Duration(seconds: 1)).then((t) {
    request.callback(http.Response('', 200));
  });

  // This is just a sample
  // http.post(request.request).then((response) {
  //   request.callback(response);
  // });
}

typedef void Callback(http.Response response);

While rather simplistic, the Model would be enhanced to include a queue of pending calls. The state or update function would add any headers as required and the platform function creates the Platform Model to be consumed by the platform. This is similar to the Widget being created and then sent to the Flutter Engine for processing. The difference in this case, is a connector that converts the Platform Model to the platform calls.

Inbound Queue and Outbound Stream (IO)

Each service now needs a way to communicate with other services, and as hinted or shown above, the Inbound Queue and Output Stream are these connectors. All messages for the service enter the Inbound Queue, and then synchronously call the update state function. Any send command issued, places a message on the outbound stream for any other service to pick up.

Managing Events with Flows

Some of you might have come to the realization there is a pending maintenance disaster hiding around the corner managing the events. One of the initial concerns I saw was how messy and unmaintainable all these events would be. Give it less than a day, and come back to the code you just wrote and you would struggle to figure out what you just wrote.

To help add context back to the abstraction, we can create a Flow object. Input the Source and Destination, then use fromSource and fromDestination to write how the flow of messages would look. Giving us this, which allows us to maintain the context of message flow.

void flow(LoginView loginView) {

  var apiService = api.instance;
  var navService = navigation.instance;

  Flow(loginView, apiService)
      .fromSource(loginPressed)
      .fromDestination(loginApiResponse);

  Flow(apiService, navService)
      .fromSource(navigateOnLogin);

}

This would be the place all the interconnecting logic of your app would be written. In another interesting twist, this helps you write a definition of everything that should be happening in that part of the app, almost making you write out a definition. Each of these flows links to a pure function that accepts a message and outputs another one. Another incidental twist, is the ease of unit testing made available here.

api.ApiPostMessage loginPressed(LoginMessage message) {
  return api.ApiPostMessage(body: message.username, url: '/oauth/login');
}

LoginResult loginApiResponse(api.ApiResponseMessage message) {
  return LoginResult(success: true);
}

Message navigateOnLogin(api.ApiResponseMessage message) {
  return navigation.LoginSuccess();
}

If you have logic you wish to share with multiple flows, add a pure function as needed that can be called upon. Flows allow you to act based upon events, rather than state, and move logic that is based upon state, into the service that holds it.

Resulting Paradigm

Combined together, your app is a collection of services and message flows. How your service reacts to messages can change your perspective when coding. Flows can effectively combine logic that was once normally scattered around the app.

It brings an acceptance that you can’t control events generated in your app and when they will occur. Using this approach you suddenly see all the glaring holes in how to handle certain events. Events that normally cause your app to crash or misbehave.

Interesting Side Effects

I came across some interesting side effects, that while not my original intention showed some promising benefits.

1) Lean into concurrency with no async keyword

I never used the async keyword on any method. Concurrency is the nature of applications. Dart doesn’t support actual concurrency per isolate, it does act concurrently if you think of events happening in different orders. With no async keyword, there is no awaiting either. If applying this to a different framework such as .NET, then that does support concurrency and hence can run into concurrency issues.

2) Group logic by feature not function

Logic is grouped by feature in Flows and not by function in classes. Rather than having a View that knows about API services, login state and more, it only cares about its own view, and any messages sent to it. Flows will group together logic combining many services without causing services to hold logic it shouldn’t own.

3) Reduce cross-cutting concerns

Listening to messages as needed, makes it easy to develop a logging service and flow. There would be no need to inject a logging service into all your services to monitor events. Write a separate logging service that listens and logs as appropriate. While not all cross cutting concerns could be alleviate, many could be.

Summary

Flow’s hold the potential to effectively manage your entire app, and I would be excited to see these developed further, including adding Reactive Extensions. Leaning into the fact mobile apps are really just a collection of concurrent services, helps reduce the overhead of managing concurrent states. The UI is a service as much as the API calls are.

Source code of a sample app is available at isoflow_example.

© 2019 Adam Pedley