In many common patterns, there is a tendency to abstract parts of the app into various sections, based off our experience in web front ends, back ends or the OS itself. Mobile apps have a screen, IO and a device they interact with, but treating these differently, is where I believe our troubles start.
Common Abstraction
First taking a look at a mobile app and how it might commonly be separated into sections, and how they are dealt with. An app receives inputs and accepts outputs from the device, maybe such as Battery Charge or location services. Further, we have our IO, think of this as our HTTP connections, or local data storage. Finally, the screen, we send it data to render, and it sends back keyboard and touch inputs.
” alt=”” aria-hidden=”true” />
Taking a different perspective of this, we can look at it this another way. The Device Services, IO and Screen are actually just all Services. Its similar to a micro services architecture, where all platform services are separate yet can send signals to each other.
In relation to our app, we could now look at an app a slightly different way.
” alt=”” aria-hidden=”true” />
Design Patterns
If we move into design patterns, there have been View based design approaches, where we architect based off the view. Or Model driven architecture that is based off the data we want the user to interact with. Regardless of approach, we come up against obstacles at a certain point.
It always breaks down when something doesn’t conform to the wonderful pattern you just adopted and makes you implement what feels like a dirty but acceptable work around.
You have Views, Models and the Device, with possibly conflicting design patterns that all merge within your app.
” alt=”” aria-hidden=”true” />
Bi-Directional vs Uni-Directional Data Flow
Devices by nature are bi-directional. Data flows in and out of services as needed. Apps themselves are bi-directional, accepting inputs from many sources and outputs to many sources. But state management becomes a problem with these bi-directional flows, especially if you allow these bi-directional flows to run concurrently to modify the same state at the same time.
Functional programming helps solves a number of issues, because the state is immutable as it goes into the function, hence can’t be modified while the function is performing actions based on the state and moves data in one direction. State being modified while being acted upon is a large source of bugs in mobile apps, based on my personal experience.
It makes more sense for functional programming in an Elm (MVU) architecture, but not so much in an MVVM pattern, which works based on a bi-directional data flow. Uni-directional flows are nice and easy to deal with as a programmer, but they live in a bi-directional world. This does lead to the clash of uni-directional code in a bi-directional system.
MVU (Model View Update)
One of my favorite uni-directional patterns is MVU. If we had no IO and instant updates, this pattern works wonderfully.
” alt=”” aria-hidden=”true” />
But, we do have side effects, such as IO calls, which leads us to this.
” alt=”” aria-hidden=”true” />
It works, but I find it more of a work around and addition, to what is otherwise a simple system. This does lead me back to the issue of uni-directional flows interacting with bi-directional flows.
Queues
Working through this problem, I came up with Queues. And while this solution looks a little more complicated, the queues can just be part of a background class, and implementing this pattern was nothing more than just one extra function. This maintains the uni-directional flow, with a dedicated bi-directional flow integration point.
” alt=”” aria-hidden=”true” />
The queue accepts all inbound requests in one place, and depending upon the message, either run’s asynchronous code or synchronous code, that produces a message into the outbound queue. The Update function then receives in order, each message.
While I will go into this in another post, I have a working version that is really simple and easy to use. An interesting side note is that if the outbound queue backs up you could run multiple update
functions before calling the view
, to save multiple render calls.
The next problem, is how about all my other services and how do I interact with them? Now I need another pattern for my other code, while using MVU for my views? This is the start of clashing patterns, and data flow directions.
I hope you can see where this is going. Why is the View any different than other IO, code blocks or device services? It’s not, I can just think of the View as a hook to another service.
Service Queue Update (SQU)
If we consider the View as a Service and just look at the View part of the MVU pattern, it sends a Widget back and receives events from touch screen input. Widget is the output, Touch Events as the input (such as onPressed
), and the View is where you define your actual view, or convert touch events to messages to send to the Update (or Queue) function.
” alt=”” aria-hidden=”true” />
This can be expanded to everything else. A single consistent pattern and uni-directional. With a different template or hook depending upon what type of service.
” alt=”” aria-hidden=”true” />
Concurrency
With this current approach ensuring a uni-directional flow with data passed as it goes through each section, makes for an easier to understand and maintain application. But as I eluded to earlier, concurrency is the other variable. We can make data go one way, but we can have multiple events coming at us at the same time, in different orders. As highlighted (in an orange arrow) here, are where concurrency occurs. The Outbound
queue, brings this back to order, only allowing one Update
at a time to complete.
” alt=”” aria-hidden=”true” />
Multiple inputs from the user, multiple subscriptions and multiple side effects can be triggered, and yet only one update will be made a time to the internal state (the model), and then passed back to the Service, such as a View.
The code to implement this is actually simpler than expected, and while lacking the subscription aspect, this shows the Model, View, Queue and Update. This is code I have working in a local sample.
class TaskView extends View<_TaskModel> {
TaskView({Key key})
: super(initial: _TaskModel(count: 0),
update: update,
view: view,
queue: queue,
key: key);
}
@immutable
class _TaskModel {
const _TaskModel({this.count});
final int count;
}
Widget view(_TaskModel model, Send send) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('The current count is:'),
Text('${model.count}'),
]),
),
floatingActionButton: FloatingActionButton(
onPressed: () => send(IncrementMessage()),
child: Icon(Icons.add))
);
}
Future queue(Message msg, Send send) async {
send(msg);
}
_TaskModel update(BuildContext context, Message msg, _TaskModel model) {
if (msg is IncrementMessage)
return _TaskModel(count: model.count + 1);
return model;
}
class IncrementMessage implements Message {}
Connecting Services via Subscriptions
Connecting services is certainly going to bring up bi-directional flow. I am all for any service being able to call any other service instance if desired. Subscriptions would be how you call any other service, and pass a callback through to run on any events, such as battery level changed. If you don’t need to worry about events, you would just call a Service within the side effect.
How you connect these to your service would be a matter of personal preference. You could use the Dependency Injection approach, pass references to services into the service itself, but then have to pass it all the way down the chain or you can use a functional approach.
Making side effects call functional, requires an extra step, of creating a partial application, and it can be worth it if unit testing, and ensuring that all methods with logic in them are functional. Make classes immutable. So when you pass them in, they don’t change state. Personally I prefer this method, because it’s less work and things break as needed if any service changes.
Future<_LoginModel> _login(BuildContext context, Message msg, _LoginModel model)
=> login(context, msg, model, myGlobalServiceInstance);
Future<_LoginModel> login(BuildContext context,
Message msg,
_LoginModel model,
Service service) async {
This makes the login function unit testable, but using partial function in your code to obtain the correct references.
Summary
My main goal is to have an underlying architecture that works for every aspect of your entire app. Particular parts of your app might require specialized patterns, but that can also be contained within a service, with the necessary bridge. Bringing your entire app down to one concept, and avoiding unnecessary abstraction, such as treating a View different than a Service, allows a significant reduction in context switching and no current seen workarounds when bi-directional concurrent patterns collide with uni-directional synchronous patterns.
I hope to come out with a larger sample app soon.
Source: buildflutter