Flux: Qt Quick with unidirectional data flow

Hello !

Today is an important day, is the day of the first article of Qt I wrote.

Have you ever heard anything about Model View Controller or Model View Delegate???? Yes obviously, but right now, we’re going to talk about another thing (yes I am funny I know) . We are going to talk about Facebook, I mean a pattern which comes from Facebook.

What are we going to talk about??

We are going to talk about the Flux pattern, this pattern says data flow should be unidirectional as opposed to the Model View Delegate pattern.

modelview-overviewModel View Delegate multi directional data flow : Credit Qt

Flux pattern representation

Flux unidirectional data flow

What is the advantages to use Flux pattern?

  1. Signals propagation is easy and do not require any copy and paste.
  2. The code is easy to read.
  3. There is low coupling

What is the Action Creator?

When a user wants to interact with the application, he want to do an “Action“. For example, he could wants to add things to a todo list, so he could launch an Action(“Things to do”);
The Action Creator is here to give Action to our dispatcher.

What is the Dispatcher?

A dispatcher takes an action and its arguments and dispatchs it through all stores.

What is the Store?

A store is like a collection of datas but it also has logic buried inside it.

What is the View?

It shows all data.

What are we going to see?

We are going to see how to write a little application using Flux.
Our application could seem to that :
Screenshot_2016-02-19-20-22-20

Let’s code !

First, we need to know what our application has to do.
It must have possibility to add a counter, increment and decrement them. We exactly have 3 actions. It is that simple !

pragma Singleton
import QtQuick 2.0

Item {
    property string add: "add";
    property string inc: "inc";
    property string dec: "dec";
}
pragma Singleton
import QtQuick 2.0

Item {
    function add() {AppDispatcher.dispatch("add", {});}
    function inc(id) {AppDispatcher.dispatch("inc", {id:id});}
    function dec(id) {AppDispatcher.dispatch("dec", {id:id});}
}

Now, we are going to see what is the AppDispatcher.

A dispatcher should take as argument the action type and a message (id for example). This dispatcher should dispatch this action as well.

class Dispatcher : public QObject {
    Q_OBJECT
public:
    Dispatcher() = default;

public slots:
    Q_INVOKABLE void dispatch(QString action, QJSValue args) {
        emit dispatched(action, args);
    }

signals:
    void dispatched(QString action, QJSValue args);
};
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "dispatcher.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    Dispatcher dispatcher;

    QQmlApplicationEngine engine;

    engine.rootContext()->setContextProperty("AppDispatcher", QVariant::fromValue(&dispatcher));

    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}

You easily could improve the dispatch behaviour. Indeed, it could be more safe to use a queue… But in this example, I just show the mechanism and try to don’t overcomplicate the app.

I remind dispatcher dispatchs through the stores.
A store is a singleton which manages all objects of the same type. Our store manages all counters in the app.

A counter is an object with an id and a value, knowing that, we easily get :

pragma Singleton
import QtQuick 2.0
import "."

Item {
    property alias model: listModel;
    property int nextId: 1;

    ListModel {
        id: listModel;

        ListElement {
            idModel: 0;
            value: 0;
        }
    }

    function getItemID(idModel) {
        for(var i = 0; i < model.count; ++i) {
            if(model.get(i).idModel == idModel)
                return model.get(i);
        }
    }

    Connections {
        target: AppDispatcher;

        onDispatched: {
            if(action === ActionType.add)
                model.append({idModel: nextId++, value:0});

            else if(action === ActionType.inc)
                getItemID(args.id).value++;

            else if(action === ActionType.dec)
                getItemID(args.id).value--;
        }
    }
}

On the onDispatched check if it is the good action, and if it is, do the required work.

Now we just need a view, as you see before, we use a “model” to store  all data, it will be the same for the view / delegate, but even if we use model view delegate, we will keep a unidirectional data flow.

The delegate will explains how an item should be rendered.
It is componed by 2 buttons (+ and -) and a value :

import QtQuick 2.0
import QtQuick.Controls 1.4

import "."

Rectangle {
    property alias text: t.text;
    property int fontSize: 10;
    signal clicked;


    MouseArea {
        anchors.fill: parent;
        onClicked: parent.clicked();
    }

    Text {
        anchors.centerIn: parent;
        font.pointSize: fontSize;
        id:t;
    }
}

import QtQuick 2.0
import "."

Rectangle{
    width: text.width;
    height: text.height;
    color: palette.window;

    Text {
        id: text;
        text:value;
        font.pointSize: mainWindow.width < mainWindow.height ? mainWindow.width / 16: mainWindow.height / 16;
    }

    Button {
        anchors.left: text.right;
        anchors.verticalCenter: parent.verticalCenter;

        color: Qt.rgba(0.4, 0.7, 0.2, 1);
        width: mainWindow.width / 10;
        height: mainWindow.height / 10;
        text: "+";
        fontSize: mainWindow.width < mainWindow.height ? mainWindow.width / 20 : mainWindow.height / 20;

        onClicked: ActionCreator.inc(idModel);
    }

    Button {
        anchors.right: text.left;
        anchors.verticalCenter: parent.verticalCenter;

        color: Qt.rgba(0.7, 0.4, 0.2, 1);
        width: mainWindow.width / 10;
        height: mainWindow.height / 10;
        text: "-";
        fontSize: mainWindow.width < mainWindow.height ? mainWindow.width / 20 : mainWindow.height / 20;

        onClicked: ActionCreator.dec(idModel);
    }
}

Yeah I know, there is some duplication of code, it is not good…
Now, we have the possibility to render items, we should print many of them.
Flux tells us datas are coming from Store, so let’s implement what Flux says!

import QtQuick 2.0
import QtQuick.Controls 1.4
import "."

Rectangle {
    width: view.contentItem.childrenRect.width;
    height: view.contentItem.childrenRect.height;

    color: palette.window;

    ListView {
        id: view;
        anchors.fill: parent;
        model: CounterStore.model;

        spacing: 10;

        delegate: CounterItem{}
    }
}
import QtQuick 2.5
import QtQuick.Controls 1.4
import "."

ApplicationWindow {
    id: mainWindow;
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    SystemPalette {
        id: palette;
    }


    Button {
        id: buttonAdd;
        anchors.verticalCenter: parent.verticalCenter;
        width: parent.width / 5;
        height: parent.height;
        text: "Add";
        fontSize: 30;
        color: Qt.rgba(0.1, 0.3, 0.7, 1.0);

        onClicked: AppDispatcher.dispatch("add", {});
    }

    Rectangle {
        color: palette.window;
        anchors.right: parent.right;
        anchors.left: buttonAdd.right;
        anchors.top: parent.top;
        anchors.bottom: parent.bottom;

        CounterView {
            anchors.centerIn: parent;
        }
    }
}

It is the end, if you have any questions, please, let me know !
Hope you enjoyed it and learned somethings !

References

Flux by Facebook
Quick Flux : Problems about MVC and introduction

Thanks !

Comments

5 responses to “Flux: Qt Quick with unidirectional data flow”

  1. Antoine MORRIER Avatar
    Antoine MORRIER

    Hello Vincent !

    Yes I know, but he explains action dispatcher without stores ^^. And I quoted him in “references” ^^.

  2. TanDo Avatar
    TanDo

    Hello,
    I have a question relating to Store and View. Is it possible for a store dispatch an action to view or it just provide data for view to read ?
    Thanks

    1. Antoine MORRIER Avatar
      Antoine MORRIER

      Hello TanDo !
      According to me (and the Schema I posted), the store doesn’t have to call the dispatcher, views are updated through signals and slots. So, it is not the duty of the store to dispatch action^^. But it is my opinion, maybe in some cases, it should be possible, but I think it is weird because it breaks unidirectional flow ^^.
      If I wasn’t be clear, please, let me know

  3. TanDo Avatar
    TanDo

    Hi, I also discuss this here: https://medium.com/@TanDo/hello-ben-lau-e5e485bd42f7#.dlqtwrb00

    You can join with us.

    Thanks

Leave a Reply