Action-Dispatcher Design Pattern for QML
Generally speaking, you should avoid creating a big source file. Break down into smaller files / modules is more readable and reusable. But that may not true for QML, due to signal propagation across several components: with more files, you may need to write extra code to link them. And that will cause trouble when refactoring.
In this article, I will explain the problem of signal propagation and propose an Action-Dispatcher design pattern to simplify QML application architecture. It is inspired by the Facebook Flux application framework.
Let’s take a simple example. Suppose you have a list of items, where each item has a delete button to remove it from the list. Once a button is pressed, it will show a confirmation dialog.
YourItem should hold a MouseArea / Button to receive mouse events. But what component should hold the dialog and perform the removal? YourListView.qml? YourWindow.qml?
Case 1: Hold in YourListView.qml
Case 2: Hold in YourWindow.qml
In this case, YourListView does not process the removeClicked signal. It just works a proxy to propagate signal.
And then imagine you got a new requirement: “Press on an item to launch another window.”
Obviously, it is not the duty of YourListView. It may become:
More and more signal will be added in product life cycle. (e.g Sorting, Edit, Clone, tag …). It may be fine if don’t refactor it. Otherwise, you should be aware of breaking a working function.
What happens if you are asked to add a TabView to hold multiple lists? Each tab will show a list of items with different filtering rules.
A lot of copy&paste code! Now you should know how troublesome it is! And this code is used to propagate signals only. Ideally, YourTabView and YourListView should not take the duty of signal proxy. It should have a way to let YourItem notify YourWindow directly.
What is the Flux Application Framework?
It is an application architecture designed by Facebook. In Flux, there are major parts: the dispatcher, the stores, and the views (React components). Controller do not exist in Flux application.
Flux eschews MVC in favor of a unidirectional data flow. When a user interacts with a React view, the view propagates an action through a central dispatcher, to the various stores that hold the application’s data and business logic, which updates all of the views that are affected.
The dispatcher exposes a method that allows us to trigger a dispatch to the stores, and to include a payload of data, which we call an action. The action’s creation may be wrapped into a semantic helper method which sends the action to the dispatcher. For example, we may want to change the text of a to-do item in a to-do list application.
The View component reads from Store but does not write to it directly. It asks Action to do so. The data flow is unidirectional.
The main different between Flux and MVC/MVVM design pattern is the separation of queries and updates. The Store is a read-only data model that supports “queries” only. It can only be “updated” through Action.
Solutions — Action-Dispatcher
I am not going to explain Store and View in this article. Please also forget the unidirectional data flow. Just focus on Action and the central Dispatcher. And see how it could resolve the problem of signal propagation and improve our code.
Step 1 — Convert Signals into Actions
ActionTypes is a constant table (singleton component) to store all the available action types. It is not recommended to name an action by combing sender and event like removeItemButtonClicked. It is suggested to tell what users do (e.g. askToRemoveItem) or what it should actually do (e.g. removeItem). You may add a prefix of scope to its name if needed. (e.g. itemRemove)
AppActions is an action creator, a helper component to create and dispatch actions via the central dispatcher. It has no knowledge about the data model and who will use it. As it only depends on AppDispatcher, it could be used anywhere.
AppDispatcher is the central dispatcher. It is also a singleton object. Actions sent by dispatch() function call will be placed on a queue. Then, the Dispatcher will emit a“dispatched” signal.
Moreover, there has a side benefit in using ActionTypes and AppActions. Since they stores all the actions, when a new developer joins the project. He/she may open theses two files and know the entire API.
EDIT: In case you feel trouble to implement the dispatch function in the AppActions.qml, you may try ActionCreator component that was added since QuickFlux 1.0.5 . It is a component that listens on its own signals, convert to message then dispatch via AppDispatcher. The message type will be same as the signal name. There has no limitation on number of arguments and their data type.
Therefore, you could rewrite the AppActions.qml above in this way:
Step 2 — Don’t Propagate Signal. Call Action Creator.
It won’t need to propagate the “clicked” and “removeClicked” signals. They are not needed any more.
Step 3 — Handle Actions in Right Place
What is the different after having applied Action-Dispatcher design pattern?
The difference is obvious. Actions are sent via the central Dispatcher to receivers. No need to propagate by other components. No need to declare unnecessary signals. Message flow is always simple:
Moreover, sender and receiver are loosely coupled. They have no knowledge about each other. The receiver does not necessary to be the ancestor of the sender. They only need to know the format of defined action type. It is a perfect condition for code refactoring.
The Action-Dispatcher design pattern is a component-to-components communication method. It works by using a central dispatcher for message delivery, so that it doesn’t need another component to work as a proxy of signal propagation. And it break down the dependence between sender and receiver. The receiver does not necessary to be the ancestor of the sender, and vice versa.
Moreover, it doesn’t tell what kind of UI triggered the action. (e.g which button was clicked). Instead, it tell what users do (e.g. ask to remove an item, but confirmation needed) / what it should actually do (e.g. remove an item from data model now). The entire API list of an application is available at actions.
There are many benefits of using this pattern :
- Clean up the code by removing unnecessary signal propagation.
- List of all available user actions in a file. (at ActionTypes.qml)
- Loose coupling design. Easy refactoring.
- Improving testability through Action Creator component.
- Simple message flow.
An implementation of Dispatcher is available at : QuickFlux Project
And there have several example programs for reference:
What do you think about this design pattern? Any question / feedback is highly welcomed.
Although this method is inspired by Facebook Flux, store and unidirectional data flow is not covered in this article. I will explain it in the next article: QML Application Architecture Guide with Flux — Medium
Q1. Why use AppDispatcher instead of listening from AppActions directly?
A1. See this article