Revised QML Application Architecture Guide with Flux
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.
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.
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 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.
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.
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.qml
│ ├── ActionTypes.qml
│ └── AppActions.qml
│ └── SystemMiddleware.qml
│ ├── MainStore.qml
│ └── RootStore.qml
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
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
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.
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.
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:
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.
Document: Store QML Type | QuickFlux
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.
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.
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.
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.
Document: Middleware QML Type | QuickFlux
It is an sample middleware that block an action and prompt a dialog to ask for confirmation before process the aciton.
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.qml stores constant values in your application.
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)
- 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.
Q1. Why use AppDispatcher instead of listening from AppActions directly?
A1. See this article