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.

Problems

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.

Component Hierarchy

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.

Component Hierarchy

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?

Flux | Application Architecture for Building User Interfaces

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.

Actions

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.
Architecture

The View component reads from Store but does not write to it directly. It asks Action to do so. The data flow is unidirectional.

Example of unidirectional data flow

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

actions/ActionTypes.qml

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)

actions/AppActions.qml

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.

YourItem.qml:

It won’t need to propagate the “clicked” and “removeClicked” signals. They are not needed any more.

Step 3 — Handle Actions in Right Place

YourWindow.qml

Comparison

What is the different after having applied Action-Dispatcher design pattern?

The event flow before using a central dispatcher (signal propagation)
The event flow after using a central Dispatcher (No signal propagation)

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:

Message flow of Action-Dispatcher

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.

Conclusion

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)
  • Reduce no. of inline Javascript code. (Those in onClicked signal)
  • 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:

https://github.com/benlau/quickflux/tree/master/examples

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

FAQ

Q1. Why use AppDispatcher instead of listening from AppActions directly?

A1. See this article

Related Articles

  1. QML Application Architecture Guide with Flux — Medium
  2. Flux: Qt Quick with unidirectional data flow
Show your support

Clapping shows how much you appreciated Ben Lau’s story.