Revised QML Application Architecture Guide with Flux

Flux Application Architecture in QML

In the previous article of “QML Application Architecture Guide with Flux”, it has introduced how to apply the concept of Flux in QML and the benefit of “Update”/“Query” separation. It gives a guideline on how to organise your QML files and write clean code by avoiding unnecessary signal propagation.

The article published on Mar 2016, it is already a year old. And the Quick Flux library used in the article has released a v1.1. It brings new concepts like Middleware, Hydration and deprecates old components like AppListener. (Reference: What is new in Quick Flux 1.1)

Therefore, I have revised the original article to adopt the new concept from Quick Flux v1.1.

The Motivation

Currently, I still don’t see anything like a standard application architecture guide available for a QML application yet. People may write their software by MVC, MVVM and any other pattern else. The most popular choice should be MVVM like approach: Declare data model and logic in a QObject written by C++ and implement GUI in QML and JavaScript.

That is not a bad approach. Easy to understand and flexible enough. But it does not tell you how to manage your QML components. And probably people may also write their application logic in inline Javascript code too( those in the onXXX code block). The result is a lot of fragmented code spread across view items. Once your application is getting more complicated. It is getting more difficult to maintain your code clean and testable.

This article intends to point out the problems and provides a solution of architecture design inspired by Facebook Flux.

The solution is based on my project, QuickFlux , a library provides central dispatcher and utility components for writing QML in a Flux way

What is Flux Application Framework?

It is an application architecture designed by Facebook. In Flux, there are three major parts: the dispatcher, the stores, and the views (React components). The controller does 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.

Architecture

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.

A Single Dispatcher

The dispatcher is the central hub that manages all data flow in a Flux application. It is essentially a registry of callbacks into the stores and has no real intelligence of its own — it is a simple mechanism for distributing the actions to the stores. Each store registers itself and provides a callback. When an action creator provides the dispatcher with a new action, all stores in the application receive the action via the callbacks in the registry.”

Stores

Stores contain the application state and logic. Their role is somewhat similar to a model in a traditional MVC, but they manage the state of many objects — they do not represent a single record of data like ORM models do. Nor are they the same as Backbone’s collections. More than simply managing a collection of ORM-style objects, stores manage the application state for a particular domain within the application.

The main difference between Flux and MVC/MVVM design pattern is the separation of queries and updates. Although View components read from Stores to render content, it doesn’t write to it directly. Instead, it asks Action to do so. Store is a read-only data model that support “query” only. It can only be “updated” through Action.

In traditional MV* application, Views read data from Models and write to them directly. It is simple, if the relationship is a one-to-one mapping. But usually it is not. User’s action on a View component may trigger the update of several models. And sometimes a model may need to update other models too. In this case, the data flow will be complicated and difficult to trace and debug.

The data flow of an MVC application — Just imaginary you are playing a ping pong game, but the no. of player is inconsistent on the different side.

However, once you have separated the queries and updates using Action and Dispatcher, the data flow will become unidirectional. The data flow always begins at delivering an Action. If this action triggers another kind of update, it will dispatch a new action to let the data flow start over again. That will simplify your application’s data flow, such that it will be easier to trace and debug.

An example of Unidirectional Data Flow

Remarks: The diagram above is just an example of particular data flow. It does not mean that you can’t dispatch a new action in store component.

For more information about Flux, I would recommend reading “A cartoon guide to Flux — Code Cartoons — Medium". Next section will talk about how to implement a QML application in a Flux way.

Flux Application Architecture in QML

. 
├── constants
│ └── Constants.qml
├── actions
│ ├── ActionTypes.qml
│ └── AppActions.qml
├── main.qml
├── middlewares
│ └── SystemMiddleware.qml
├── stores
│ ├── MainStore.qml
│ └── RootStore.qml
└── views

It is the proposed application structure of a QML application written in a Flux way. An example project is available at : https://github.com/benlau/quickflux/tree/master/examples/todo

A TODO list program implemented in Flux Application Architecture

actions/ActionTypes.qml

ActionTypes is a constant table (singleton component) to store all the available action types in an application.

It is not recommended to name an action by combing sender and event (e.g removeItemButtonClicked). It is suggested to tell what users do (e.g. askToRemoveItem, remove an item but it needs to prompt a dialog for confirmation) or what it should actually do (e.g. removeItem). You may add a prefix of scope to its name if needed. (e.g. itemRemove)

Document: KeyTable QML Type | QuickFlux

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 in your code.

Example of use of Action Creator

AppDispatcher is the central dispatcher. It is also a singleton object. Actions sent by dispatch() function call will be placed on a queue. If there have no pending actions, the dispatcher will emit a“dispatched” signal immediately. It is designed to avoid out-of-order message processing.

Moreover, there has a side benefit in using ActionTypes and AppActions. Since they contain all the actions in an application when a new developer joins the project. He/she may open theses two files and know the entire API.

AppActions Example

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 for 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 a number of arguments and their data type.

Therefore, you could rewrite the AppActions.qml above in this way:

Document:

  1. ActionCreator QML Type | QuickFlux
  2. AppDispatcher QML Type | QuickFlux

/stores

Stores contain application data, state and logic. Somehow it is similar to the View Model in MVVM. But due to the principle of unidirectional flow. It doesn’t export update methods to Views. It is read-only to Views. “Updates” should only be done via Actions.

The Store Architecture in the “todo” example

Document: Store QML Type | QuickFlux

/stores/MainStore.qml

The original Flux implementation uses multiple singleton Stores while Redux only uses a single store for application state. What approach should we take in QML?

From my experience with several applications, a single singleton Store is more easy to manage. Because it may have store-to-store data dependence, holding multiple singleton store object will be a big trouble due to the QTBUG-49370.

MainStore.qml

/stores/RootStore.qml

Although it is suggested to hold the store component in singleton object, it has a negative drawback on testability. Singleton object is usually quite different to write test cases. Therefore, it is not suggested to implement the property fields in the MainStore.qml . It should hold the implementation detail on its praent type, RootStore.qml.

Because you could instantiate in a unit test. The instance is flashy created and doesn’t be influenced by another test case.

RootStore.qml

/views

It is the folder for view components. It is not recommended to put your application logic within view components. Because inline code for handling user event can be hard to trace and test. They are not centralized in one source file. The dependence is not obvious and therefore it is easy to be broken during refactoring.

In order to simplify your inline code, you may follow the “Tell, Don’t Ask” principle.

That is, whatever you have received an UI event, you should tell Action Creator what you want to do, do not ask Store for questions and make a decision.

However, there has an exception case. If the inline code is responsible for handling animation / transition effect, it will be better to leave it to view components.

/middlewares

The middleware in Quick Flux is similar to the one in Redux and those from server libraries like Express and Koa. It is some code you can put between the Dispatcher and Stores. It could modify/defer /remove received actions and insert new actions that will dispatch to Store components.

Users may use it for logging, crash reporting, talking to asynchronous components like FileDialog / Camera etc.

So that Store components remain “pure”, it holds application logic only and always return the same result for the same input. It is easier to write test cases.

Quick Flux Application Data Flow with Middleware

Document: Middleware QML Type | QuickFlux

/middlewares/DialogMiddleware.qml

It is an sample middleware that block an action and prompt a dialog to ask for confirmation before process the aciton.

The UI effect

/main.qml

That is the entry point of a QML application. You may setup the middleware and launch the application window.

Document: MiddlewareList QML Type | QuickFlux

/constants/Constants.qml

Constants.qml stores constant values in your application.

Conclusion

When people talk about Flux, they usually emphasis on unidirectional data flow. But in fact, the principle of “queries” and “updates” separation is easier to understand for QML developers. By using the principle with Actions, Dispatcher and Stores, it could improve QML code with the following benefits:

  • Clean up the code by removing unnecessary signal propagation. (More information at this article )
  • List of all available user actions in a file. (at ActionTypes.qml)
  • Reduce no. of inline Javascript code in Views. (Those in onClicked signal)
  • Loose coupling design. Easy refactoring.
  • Improving testability through Action Creator component.
  • Unidirectional Data Flow (Simple Message Flow)

If you have any questions / suggestions for this guide, please feel free to leave a comment here.

FAQ

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

A1. See this article

References:

  1. What the Flux? (On Flux, DDD, and CQRS) — Jack Hsu
  2. Action-Dispatcher Design Pattern for QML
  3. A cartoon guide to Flux — Code Cartoons
  4. Flux: Qt Quick with unidirectional data flow