Furkanzmc / QML-Coding-Guide

A collection of good practices when writing QML code - https://doc.qt.io/qt/qtqml-index.html
The Unlicense
532 stars 79 forks source link

C++ Integration #9

Open daravi opened 3 years ago

daravi commented 3 years ago

Hello. One point that I see missing from both the official Qt documentation and this guide is distinguishing C++ model types and C++ visual types. I think most users use the visual types provided by QtQuick or Qt Design Studio instead of creating their own visual components. And they use C++ for model / data backend. So it is important to emphasize what is the most natural integration type for this use case.

Usually data needs to be persistent and not be created / destroyed when you navigate between different views. Furthermore you sometimes need to initialize the object in some specific way. For example pass a handle to a data base or to an IPC (inter-process communication) object to it. This means that the only integration method that works in this situation is using qmlRegisterSingletonInstance. (I don't think lifetime concerns is a problem since these days everyone is familiar with unique_ptr, whereas if you need to initialize your object in some way or pass some specific arguments to it, having the QML engine create it is either impossible or very hard with no benefits). In summary, if you really want your model to be independent of your view, you want to also make the lifetime independent.

It seems to me that the guideline should be presented in this way:

The reason I think this should be emphasized is that all Qt documentation emphasize the integration methods that create the object from QML. This is not practical for more than 90% of models in my experience due to the above limitations and leads to wasted time trying to fit a square peg in a round hole.

Furkanzmc commented 3 years ago

Hi. Thanks for the suggestion.

I agree that we should have something for the non-visual types as well. Although I mostly agree with what you said, I don't agree with this part:

If your C++ type is a model then use qmlRegisterSingletonInstance and keep your instance as a unique_ptr or parent it to the engine. If you want to duplicate your data for each view that uses the model, or if your model does not need to be persistent between views then use qmlRegisterType

Maybe If you clarify what you mean by ... or if your model does not need to be persistent between views, it would be useful for me. Because just because the data needs to be shared between different views, doesn't mean it has to be a singleton.

The only reason I see where a singleton is useful is when the data needs to persist throughout the application life time. Not just between views. A very simple example, I can have a on-boarding process where the user name has to be shown on each page, but that doesn't warrant a singleton use. In this case, the data should be passed to the next view as necessary.

OlivierLDff commented 3 years ago

In this case, the data should be passed to the next view as necessary.

In cases like that i sometimes use an attached property that behave like the Material attached property from QtQuick.Controls.Material.

Like that i can extend my data object without touching all the views on the way.

Simple example i use in my app.

I defined an attached property View, so each qml file can juste call View.screenUid or View.pageUid to know what to render, etc... (and mostly what command generate to speak with c++ backend).

But this approach require lots of boiler plate code to inject property to children. With good macro View implementation can look like that:

class View : public QObject
{
    Q_OBJECT

public:
    View(QObject* parent = nullptr)
        : QObject(parent)
    {
    }

    QML_ATTACHED_OBJECT(View, ScreenUid, PageUid);

    QML_ATTACHED_PROPERTY(int, screenUid, ScreenUid, 1);
    QML_ATTACHED_PROPERTY(int, pageUid, PageUid, 1);
};

Ugly macro looks like that :

#define QML_ATTACHED_OBJECT_DECORATION(Class)                                                                                           \
private:                                                                                                                                   \
    QML_ATTACHED(Class);                                                                                                                   \
                                                                                                                                           \
public:                                                                                                                                    \
    static Class* qmlAttachedProperties(QObject* object)                                                                                   \
    {                                                                                                                                      \
        return new Class(object);                                                                                                          \
    }                                                                                                                                      \
                                                                                                                                           \
private:

#define QML_ATTACHED_OBJECT_BITSET(Class, ...)                                                                                          \
private:                                                                                                                                   \
    enum class AttachedInheritedProperties                                                                                                 \
    {                                                                                                                                      \
        __VA_ARGS__,                                                                                                                       \
        COUNT                                                                                                                              \
    };                                                                                                                                     \
    std::bitset<static_cast<std::size_t>(AttachedInheritedProperties::COUNT)> _ownedProperties;                                            \
    std::bitset<static_cast<std::size_t>(AttachedInheritedProperties::COUNT)> _initializeProperties;                                       \
                                                                                                                                           \
private:

#define QML_ATTACHED_OBJECT(Class, ...)                                                                                                 \
    QML_ATTACHED_OBJECT_DECORATION(Class)                                                                                               \
    QML_ATTACHED_OBJECT_BITSET(Class, __VA_ARGS__)

#define QML_ATTACHED_PROPERTY(__type, attribute, Attribute, __def)                                                                      \
protected:                                                                                                                                 \
    Q_PROPERTY(__type attribute READ attribute WRITE set##Attribute RESET reset##Attribute NOTIFY attribute##Changed)                      \
private:                                                                                                                                   \
    static constexpr __type default##Attribute()                                                                                           \
    {                                                                                                                                      \
        return __def;                                                                                                                      \
    }                                                                                                                                      \
    __type _##attribute = default##Attribute();                                                                                            \
                                                                                                                                           \
public:                                                                                                                                    \
    __type attribute()                                                                                                                     \
    {                                                                                                                                      \
        if(!attribute##Owned() && !attribute##Initialized())                                                                               \
        {                                                                                                                                  \
            set##Attribute##Initialized();                                                                                                 \
            internalSet##Attribute(parent() ? find##Attribute##InParents(parent()->parent()) : default##Attribute());                      \
        }                                                                                                                                  \
        return _##attribute;                                                                                                               \
    }                                                                                                                                      \
    void set##Attribute(__type value)                                                                                                      \
    {                                                                                                                                      \
        _ownedProperties.set(static_cast<std::size_t>(AttachedInheritedProperties::Attribute));                                            \
        set##Attribute##Initialized();                                                                                                     \
        internalSet##Attribute(value);                                                                                                     \
        propagate##Attribute();                                                                                                            \
    }                                                                                                                                      \
    void reset##Attribute()                                                                                                                \
    {                                                                                                                                      \
        if(!_ownedProperties.test(static_cast<std::size_t>(AttachedInheritedProperties::Attribute)))                                       \
            return;                                                                                                                        \
                                                                                                                                           \
        _ownedProperties.reset(static_cast<std::size_t>(AttachedInheritedProperties::Attribute));                                          \
        set##Attribute##Initialized();                                                                                                     \
        internalSet##Attribute(parent() ? find##Attribute##InParents(parent()->parent()) : default##Attribute());                          \
        propagate##Attribute();                                                                                                            \
    }                                                                                                                                      \
                                                                                                                                           \
Q_SIGNALS:                                                                                                                                 \
    void attribute##Changed();                                                                                                             \
                                                                                                                                           \
private:                                                                                                                                   \
    void propagate##Attribute() const                                                                                                      \
    {                                                                                                                                      \
        propagate##Attribute(qobject_cast<QQuickItem*>(parent()));                                                                         \
    }                                                                                                                                      \
    void propagate##Attribute(QQuickItem* item) const                                                                                      \
    {                                                                                                                                      \
        if(!item)                                                                                                                          \
            return;                                                                                                                        \
                                                                                                                                           \
        for(auto* const child: item->childItems())                                                                                         \
        {                                                                                                                                  \
            auto* const attached = qobject_cast<View*>(qmlAttachedPropertiesObject<View>(child, false));                                   \
            if(attached)                                                                                                                   \
            {                                                                                                                              \
                if(!attached->inherit##Attribute(_##attribute))                                                                            \
                    continue;                                                                                                              \
            }                                                                                                                              \
            propagate##Attribute(child);                                                                                                   \
        }                                                                                                                                  \
    }                                                                                                                                      \
    bool inherit##Attribute(__type value)                                                                                                  \
    {                                                                                                                                      \
        if(attribute##Owned())                                                                                                             \
            return false;                                                                                                                  \
        set##Attribute##Initialized();                                                                                                     \
        internalSet##Attribute(value);                                                                                                     \
        return true;                                                                                                                       \
    }                                                                                                                                      \
    __type find##Attribute##InParents(QObject* obj) const                                                                                  \
    {                                                                                                                                      \
        if(!obj)                                                                                                                           \
            return default##Attribute();                                                                                                   \
                                                                                                                                           \
        auto* const attached = qobject_cast<View*>(qmlAttachedPropertiesObject<View>(obj, false));                                         \
        if(attached && attached->attribute##Initialized())                                                                                 \
            return attached->_##attribute;                                                                                                 \
                                                                                                                                           \
        return find##Attribute##InParents(obj->parent());                                                                                  \
    }                                                                                                                                      \
    void internalSet##Attribute(__type value)                                                                                              \
    {                                                                                                                                      \
        if(value != _##attribute)                                                                                                          \
        {                                                                                                                                  \
            _##attribute = value;                                                                                                          \
            Q_EMIT attribute##Changed();                                                                                                   \
        }                                                                                                                                  \
    }                                                                                                                                      \
    bool attribute##Owned() const                                                                                                          \
    {                                                                                                                                      \
        return _ownedProperties.test(static_cast<std::size_t>(AttachedInheritedProperties::Attribute));                                    \
    }                                                                                                                                      \
    bool attribute##Initialized() const                                                                                                    \
    {                                                                                                                                      \
        return _initializeProperties.test(static_cast<std::size_t>(AttachedInheritedProperties::Attribute));                               \
    }                                                                                                                                      \
    void set##Attribute##Initialized()                                                                                                     \
    {                                                                                                                                      \
        _initializeProperties.set(static_cast<std::size_t>(AttachedInheritedProperties::Attribute));                                       \
    }

Hope this can help someone. This approach sit in between singleton & context property. I find it better than context property because it is more explicit.

daravi commented 3 years ago

Maybe If you clarify what you mean by ... or if your model does not need to be persistent between views, it would be useful for me. Because just because the data needs to be shared between different views, doesn't mean it has to be a singleton.

I agree that that part could be worded better. But I mean if your data needs to exist before your view is created or after it has been destroyed.

The only reason I see where a singleton is useful is when the data needs to persist throughout the application life time.

Another common case for me is when trying to select between two view delegates based on some property value. In that case I would usually use a Loader element and set the item property. However if I navigate away and navigate back to this view the view is recreated dynamically by the Loader. Which means any model data used by the loaded element is lost since the model is not a singleton and is re-created when the delegate is created.

And if I am setting up live reloading (which I think all QML developers should set up, at least until the Qt company offers us a better solution), then I need my application view to persist between reloads. (instead of having to navigate back to the page I was on)

A very simple example, I can have a on-boarding process where the user name has to be shown on each page, but that doesn't warrant a singleton use. In this case, the data should be passed to the next view as necessary.

Wouldn't you just use a property in that case? I agree if the above do not apply then you would just register as a regular type to be created by the QML engine.

So to summarize I think these are the cases where you wouldn't need a singleton instance created from C++: -> You don't need to initialize your backend (e.g. connect to a database, to a TCP socket, pass in configurations, etc.) AND -> You don't need to setup live reloading AND ( -> You don't need your data to be persistent if the view is destroyed and recreated OR -> You don't dynamically create any views OR -> You never navigate away from the dynamically created views ) AND* -> It logically doesn't matter if you have multiple instances of the model

In my experience, following the above decision chart, the only non-singleton instance models I am left with are the helper classes that offload pure computation (without any data) to my C++ backend.

Furkanzmc commented 3 years ago

@OlivierLDff I definitely agree that attached properties is a useful tool for this purpose. And there's not much information about it and its use cases. I think the snippet would prove useful when included in the right context. Let me know if you'd be willing to add something about this to the guide, otherwise I'll try to use the feedback from this issue to write something.


@daravi

Another common case for me is when trying to select between two view delegates based on some property value. In that case I would usually use a Loader element and set the item property. However if I navigate away and navigate back to this view the view is recreated dynamically by the Loader. Which means any model data used by the loaded element is lost since the model is not a singleton and is re-created when the delegate is created.

I wouldn't choose a singleton to persist that information. In the project I'm working on, we also have very similar cases where a state of a control (e.g a group is expanded/collapsed) is important to remember. But this information is persisted somewhere in the core with appropriate data structures. The next time the same window is opened and we are showing the same group, we get that in the context (not the QML context) of the window. We already provide data to the view about the window (e.g which controls are shown, what data is displayed), and part of that data is that expanded/collapsed state.

Wouldn't you just use a property in that case? I agree if the above do not apply then you would just register as a regular type to be created by the QML engine.

My bad, it was meant to be a very simple example but its simplicity killed its effectiveness at trying to prove my point. You are right, in that case I would just use a property. And that is my point. No matter how complex the data gets, I would not choose to provide a singleton just because I need some data to persist between different views.

In my experience, following the above decision chart, the only non-singleton instance models I am left with are the helper classes that offload pure computation (without any data) to my C++ backend.

I would argue for the opposite of that where singletons are used almost always for helper functions, and non-singleton instances are used for data. The only exception that I see to this is when the data needs to persist throughout the application life time, and not just between the views. The more singletons that you have, the harder it would be to reason about data and ownership. I find that QML has the most flexibility and easiest to reason with (especially in very large projects) If they are individual isolated components that don't rely on any global state.

This doesn't say anything about how you choose to store that data that you want available in multiple views. That data could be a singleton in the C++ side, but it doesn't necessarily mean that it has to be a singleton in the QML side. When data is injected using instance models, or by passing as a property, the ease of mocking would be much better than when a singleton is provided. For example, a designer can do their prototyping with QML scene with minimal access to the C++ data model if they just provide a mock data themselves.

In your example with the database connection, this connection can be established in an instance-object as well. And it'd be easier to reason about the life of the connection. Or these instance objects can internally connect to an internal C++ singleton to share a connection and close it if no clients are using it. Although I'm not arguing against the use of singletons in the C++ side, I'm not that keen on advocating it in the QML side.

As far as choosing an integration type goes, Qt's documentation provides a pretty good char. I think it'd be useful to have concrete examples of this in the guide with different use cases so that people can get a better understanding. It'd be great if you are willing to provide a good example of your use case instead of making a generalized advice for choosing singletons.

Maybe it was not your intention but your examples and your chart made me think If I follow it, I'll end up with a lot of singleton in my project.

OlivierLDff commented 3 years ago

@Furkanzmc For attached properties i think the context of screen id, or page id can be used. Or the context of theme like Material does. To propagate a theme. It can be generalized that attached properties should be used to propage a context without worrying about "piping" every component.


I want to add my use case about singleton, I feel that i'm using lots of singleton too. Most of the time, i create something like an Application singleton that store all my data Store. Typically it would store something like a Document, and a ViewState.

And then in my qml folder i have something like:

qml
 ├─Controls
 └─Views

Everything in Controls shouldn't use Application. Views are aware of Application.

If a designer want to moc the Application singleton, he can create one and add it to the qml import path. But in my apps i use https://github.com/OlivierLDff/QaterialHotReload to do hot reload. So the Application is registered from c++, and i can open any qml file that is expected to render correctly with Application.

Of course this is overly simplified example.

My design & development goal using such an architecture is to make qml component independant, without wiring between them. With such a design, it also keep nice & clean separation between model / view. Because the app is a singleton, it can be runned without qml and expected to have all it's functionality remaining. This is not useful for a mobile application, but a desktop top, this is cool, since the application may also offer some network service that you want to run without ui, or in headless environment.

daravi commented 3 years ago

Hello again @Furkanzmc and @OlivierLDff. Firstly apologies for my late reply.

Edit: Sorry this comment turned out to be long. I have highlighted what I think are the main points of disagreement.

In the project I'm working on, we also have very similar cases where a state of a control (e.g a group is expanded/collapsed) is important to remember. But this information is persisted somewhere in the core with appropriate data structures. The next time the same window is opened and we are showing the same group, we get that in the context (not the QML context) of the window. We already provide data to the view about the window (e.g which controls are shown, what data is displayed), and part of that data is that expanded/collapsed state.

How would your model last then? The QML engine decides when to create and destroy the model when it is not registered as a singleton. I am assuming you use C++ singletons? (see my discussion below)

And that is my point. No matter how complex the data gets, I would not choose to provide a singleton just because I need some data to persist between different views.

Ok I see what you mean. I should have emphasized that sharing data between views is not the main reason to use singleton. Though sometimes passing data through many elements between two components that are visually far apart but are logically close means all the components in between become less reusable since now they are polluted with this data being passed around. But I agree with you that it is not a very good reason since it adds global state. The main point I was trying to make in that paragraph was to have your data persists when you navigate away from a dynamically created view, and then navigate back to the same view. The view is destroyed and recreated. Whereas you might want your model's lifetime to be persistent.

I would argue for the opposite of that where singletons are used almost always for helper functions

Sorry I should have clarified. I am not saying QML singletons are not good for that, they are. I was just saying that with the other use cases that need model life-time be different than view life-time, I cannot avoid using a singleton. Whereas with the helper functions, since I don't have any persistent data, I can have the view create and destroy the model and drive model's lifetime.

The more singletons that you have, the harder it would be to reason about data and ownership I totally agree with this and that is why I agree that the first point in this comment was not a very good reason (sharing data between distant views). And I personally try to only use the singleton model in the one view that mainly needs it, and pass the necessary data to the delegates or other nodes as needed.

I find that QML has the most flexibility and easiest to reason with (especially in very large projects) If they are individual isolated components that don't rely on any global state. I totally agree. However what is the alternative? If you only use the singleton models the same way you would use a non-singleton model, meaning that you import it in the view that needs it and then pass the data around to any other visual elements that need the data, then you have not added any global state. This does however require the programmer to do the right thing.

That data could be a singleton in the C++ side, but it doesn't necessarily mean that it has to be a singleton in the QML side

I would argue against this for 2 reasons. One that it adds an extra level of indirection, your QML creates some C++ accessor object that uses another singleton backend. And the more important reason. It means you have to add a singleton on the C++ side, which I would argue is more evil than a QML singleton instance. I know a QML singleton instance's lifetime, and I know the QML engine's lifetime, so I can ensure the instance is created before the engine and is destroyed after the engine is destroyed. The issue with C++ singletons is that they since they are generally created using static variables they might be created before main or destroyed after main, resulting in all sorts of random initialization / destruction / link order issues (SIOF). (and they also add global state on the C++ side as the QML ones do on QML side)

When data is injected using instance models, or by passing as a property, the ease of mocking

This is something I have not considered as we do not yet unit test our Qt Quick types. But I am wondering, would this be an issue if the user also follow the guideline mentioned above to not share the singleton model between views, and use it only in the view that needs it and pass the appropriate data to other views that need it from that main view like how you describe for non-singleton types?

this connection can be established in an instance-object as well

Would do you mean by "instance-object"? If you are talking about a type registered as non-singleton then I don't follow how that is possible. Let's say you need to read a port number from some configurations file. How would you then pass this port number to QML and how would QML pass this to the constructor of the model? You could use another model for the configurations, and then pass those as properties to the model and have it inherit from QQmlParserStatus, but that is a lot more complexity and doesn't solve the global state issue because you have to make the configs model now a singleton.

As far as choosing an integration type goes, Qt's documentation provides a pretty good chart

I agree that is a good chart. But it does not address the cases mentioned in my post, namely when lifetime of your model is different than your view, or when your view cannot instantiate your model because you need some initialization (e.g. passing a config value to the constructor).

I think it'd be useful to have concrete examples of this in the guide with different use cases so that people can get a better understanding

That would be great. Maybe I am missing something about how I should be doing the integrations.

It'd be great if you are willing to provide a good example of your use case instead of making a generalized advice for choosing singletons.

My main use cases: 1- I want my data to persist between reloads of single elements or the entire view 2- I simply cannot create the type from QML since I need to pass a config value to my constructor 3- There should never be multiple copies of the data. Even if the view is duplicated. For example I have some sensor values that do not make sense to be duplicated because they are the same values coming from the same sensors

Maybe it was not your intention but your examples and your chart made me think If I follow it, I'll end up with a lot of singleton in my project.

That is actually why I am very interested in this discussion and am wondering why the Qt documentation does not emphasize this. I have been wondering if I am missing something and have talked with different people but have received mixed feedback. I get the feeling that the difference between my approach and yours specifically is that you have C++ singletons and I prefer to have QML "singleton instances" (which are not singletons on C++ side) for the above mentioned reasons. I do not see any other option beyond these two approaches that would address the above object lifetime / construction limitations.

Furkanzmc commented 3 years ago

In the project I'm working on, we also have very similar cases where a state of a control (e.g a group is expanded/collapsed) is important to remember. But this information is persisted somewhere in the core with appropriate data structures. The next time the same window is opened and we are showing the same group, we get that in the context (not the QML context) of the window. We already provide data to the view about the window (e.g which controls are shown, what data is displayed), and part of that data is that expanded/collapsed state.

How would your model last then? The QML engine decides when to create and destroy the model when it is not registered as a singleton. I am assuming you use C++ singletons? (see my discussion below)

It's my bad that I kept saying singletons. The data is owned by the C++ side, and can be persisted in however way you like. You can store them in a singleton, you can keep them in anonymous namespace, or you can fetch it from a database. IN our case, the data always lives in the core. So, we fetch it and show it in the QML side using models, be it with a wrapper QObject or using roles, or some other way (See my post here, we are not actually using the function side of it yet, but the data is directly written as QML properties. But this is a use case outside of models/views.)

And that is my point. No matter how complex the data gets, I would not choose to provide a singleton just because I need some data to persist between different views.

The main point I was trying to make in that paragraph was to have your data persists when you navigate away from a dynamically created view, and then navigate back to the same view. The view is destroyed and recreated. Whereas you might want your model's lifetime to be persistent.

I agree with this, and we are doing this in our project. I also understand this is the way to go when you enable hot reloading in QML. I think we both agree this is correct, but we have a difference of opinion about how to do it.

I find that QML has the most flexibility and easiest to reason with (especially in very large projects) If they are individual isolated components that don't rely on any global state.

I totally agree. However what is the alternative? If you only use the singleton models the same way you would use a non-singleton model, meaning that you import it in the view that needs it and then pass the data around to any other visual elements that need the data, then you have not added any global state. This does however require the programmer to do the right thing.

In our project, we use a modified version of the redux architecture. So, it's very easy for us to mock data and provide it to a certain view. Even if the data is coming from a central place (be it singleton or not) in the C++ side, this gives us the flexibility to still mock data. If a singleton was being used, then mocking data in the singleton would change it everywhere.

Another reason that I avoid singletons is because we share our code base with designers. In our team, designers and developers work on the same QML code base. Developers create the business logic, some starter components for designers to mock, and that's it. The rest is handled by the designers. But they tend to check out code in other places as well. And I want the code to be clean of any non-QML like structures (singletons, context properties, functions.).

That data could be a singleton in the C++ side, but it doesn't necessarily mean that it has to be a singleton in the QML side

I would argue against this for 2 reasons. One that it adds an extra level of indirection, your QML creates some C++ accessor object that uses another singleton backend. And the more important reason. It means you have to add a singleton on the C++ side, which I would argue is more evil than a QML singleton instance. I know a QML singleton instance's lifetime, and I know the QML engine's lifetime, so I can ensure the instance is created before the engine and is destroyed after the engine is destroyed.

I also avoid singletons in C++ side, but since the conversation was revolving around it I kept referencing it. My bad. You are absolutely right. But, if you only have one engine then the singleton you register there is essentially like a C++ singleton because it's going to be alive as long as the application/engine is.

When data is injected using instance models, or by passing as a property, the ease of mocking

This is something I have not considered as we do not yet unit test our Qt Quick types. But I am wondering, would this be an issue if the user also follow the guideline mentioned above to not share the singleton model between views, and use it only in the view that needs it and pass the appropriate data to other views that need it from that main view like how you describe for non-singleton types?

I think this might just be the only acceptable type of singleton usage in QML. There would be one and only one file that accesses this singleton, and it would be it's responsibility to pass it to other components. And those components can pass down other data that they get from the singleton down to other components.

this connection can be established in an instance-object as well

Would do you mean by "instance-object"? If you are talking about a type registered as non-singleton then I don't follow how that is possible. Let's say you need to read a port number from some configurations file. How would you then pass this port number to QML and how would QML pass this to the constructor of the model? You could use another model for the configurations, and then pass those as properties to the model and have it inherit from QQmlParserStatus, but that is a lot more complexity and doesn't solve the global state issue because you have to make the configs model now a singleton.

Our docking implementation works in a similar way. If you look into our main window code, you'd only see this:

// A bunch of other code
// ..
// MainDockController is a QQuickItem type in the C++ side.
MainDockController {
  model: DockControllerModel {}
}

When MainDockController is instantiated, it registers itself with a registry. This registry does not expose any data members. It holds the registered main dock controllers in the anonymous name space. We could have multiple MainDockControllers, and when the QML engine destroys it, it would be removed from the registry as well.

During initialization, we read a JSON file to get all the state data. This is when a routine reads the JSON file, gets the docked windows, and passes this information to MainDockController's model. We have two ways of mocking data:

  1. Data provided by JSON
  2. Data declared in QML
    MainDockController {
    model: DockControllerModel {
    DummyData { }
    DummyData { }
    }
    }

This way, QML is not exposed any singletons, and we have the freedom to persist data however we want in the C++ side. This is one example, in some other cases, the model itself might fetch data from QML side according to some key.

It'd be great if you are willing to provide a good example of your use case instead of making a generalized advice for choosing singletons.

My main use cases:

1- I want my data to persist between reloads of single elements or the entire view 2- I simply cannot create the type from QML since I need to pass a config value to my constructor 3- There should never be multiple copies of the data. Even if the view is duplicated. For example I have some sensor values that do not make sense to be duplicated because they are the same values coming from the same sensors

I think these could be addressed using the strategies I mentioned above except the third one. This is highly specific, and it could again be achieved without a QML singleton.

OlivierLDff commented 3 years ago

be it with a wrapper QObject or using roles, or some other way

Very interesting article. I am currently adding dynamic function to QObject in QJSEngine, and it is interesting to see how you handled variadic functions. I want to share with you how i did myself. Sorry if this is outside of the scope of this issue.

QJSEngine *engine = qjsEngine(this);
const auto jsValue = engine->newQObject(this);
const QString f = "myFunction";
const auto script = QString("(function (...args) { this.__callFunction('%1', args) })").arg(f);
const auto jsFunction = engine->evaluate(script);
jsValue.setProperty(f, jsFunction);

And the Q_INVOKABLE void __callFunction(QString, QVariantList); do the trick.


To come back to the subject, what do you think of the approach taken by quickflux? It totally embrace using singleton, like the Action singleton, or the Store singleton. So you can have:

Button {
  text: `increment counter ${Store.counterName}`
  onClicked: () => Actions.incrementCounter(Store.counterName)
}

In your implementation how to you handle api call that modify your model? With the flux approach

Cons of this approach, is when doing hot reload, if you change a singleton, then you need to restart the app.

Furkanzmc commented 3 years ago

I want to share with you how i did myself.

Thanks for sharing. ...args approach is much better than my use of arguments. @OlivierLDff


To come back to the subject, what do you think of the approach taken by quickflux?

I think the use of actions as a singleton is OK since it only exposes functions and not data. I think flux/redux architecture is powerful, and we are using something similar (but not strictly the same) design. I haven't used quickflux before. I just took a brief look at it and the examples. Looks to me that quickflux approach is similar to what you and daravi doing as well.

I think a similar approach is fine as long as the data is passed down from the MainWindow to TodoList instead of referencing MainStore all over the place. But in the todo list example, TodoList.qml contains VisualModel which gets its model directly from the store. I would prefer to see something like this in MainWindow.qml instead:

Window {
    width: 480
    height: 640
    visible: true

    ColumnLayout {
        anchors.fill: parent
        anchors.leftMargin: 16
        anchors.rightMargin: 16

        // ------------

        TodoList {
            model: VisualModel {
                model: MainStore.todo.model
            }
            Layout.fillWidth: true
            Layout.fillHeight: true
        }

        // ------------
}

Good thing is, this and the approach used in the example are not actually significantly different. It's just a simple refactoring effort as long as your code base is small or well structured. With this version, there can now be another TodoList with different model.

OlivierLDff commented 3 years ago

I have 2 question about your approach:


I see your goal is to inject model/data from only one file right? In more complexe case, would you recommend using strong property type to pass property thru files?

// ...
TodoList {
  todoModel: MainStore.todo.model
}
// ...

and then in TodoList

import "../stores" as Stores
Item {
  property Store.TodoStore todoModel
  // ...
}

or

Item {
  property var todoModel
  // ...
}

Second question, is for hot reloading, if I only want to open the TodoList.qml, then no model will be there by default. How would you handle this case? Maybe simple approach would be to create a TodoListTest.qml that look like that:

// TodoListTest.qml
TodoList {
  todoModel: MainStore.todo.model
}
Furkanzmc commented 3 years ago

I see your goal is to inject model/data from only one file right? In more complexe case, would you recommend using strong property type to pass property thru files?

Exactly. I find this approach to work better and provide more flexibility.

// ...
TodoList {
  todoModel: MainStore.todo.model
}
// ...

and then in TodoList

import "../stores" as Stores
Item {
  property Store.TodoStore todoModel
  // ...
}

or

Item {
  property var todoModel
  // ...
}

I always prefer the static type approach, but in this particular case it might be weird because if you want to provide another model to this one, that also has to inherit from TodoStore. I don't know if TodoStore is can be instantiated in another way.

Second question, is for hot reloading, if I only want to open the TodoList.qml, then no model will be there by default. How would you handle this case? Maybe simple approach would be to create a TodoListTest.qml that look like that:

// TodoListTest.qml
TodoList {
  todoModel: MainStore.todo.model
}

No view is useful to view on its own, you always need to provide a model to it anyways. So, personally, I don't see that as a problem. Your approach with TodoListTest.qml would work and I would do something similar. I'd just have a temporary file to use qmlscene on it for quick prototyping.

OlivierLDff commented 3 years ago

I always prefer the static type approach, but in this particular case it might be weird because if you want to provide another model to this one, that also has to inherit from TodoStore. I don't know if TodoStore is can be instantiated in another way.

So what type would you use? var or QtObject? I always feel i'm doing something wrong when I'm using var and that there is a better choice.

Furkanzmc commented 3 years ago

I always prefer the static type approach, but in this particular case it might be weird because if you want to provide another model to this one, that also has to inherit from TodoStore. I don't know if TodoStore is can be instantiated in another way.

So what type would you use? var or QtObject? I always feel i'm doing something wrong when I'm using var and that there is a better choice.

I would choose TodoStore when possible, and if not I'd resort to QtObject. I almost never use var for types that QML can already represent. If a C++ type is being used in the QML side, it should be registered so that the type can be used for properties. Otherwise things get confusing, especially as your code base grows.

OlivierLDff commented 1 year ago

To bring up the subject about the use of singleton, I now think that every use case where we wanted to use a singleton should now use attached properties, especially with new QQuickAttachedPropertyPropagator that standardize it and make it easy to use.

So I think CI-2: Use Singleton for Common API Access can be replaced with CI-2: Use Attached properties for Common API Access. I think that the Qt example with Theme is really self explainatory.

I think it can also be a good fit in CI-3: Prefer Instantiated Types Over Singletons For Data. One could imagine following use case:

ApplicationWindow
{
  // For mock purpose we can do it that way and overwrite the model with our own
  MyAttachedData.paletteColors = PaletteColorsModel {
      testData: "#123456"
  }

  // Or in production, let's say your data is coming from a cpp color model palette that gets loaded from a database or anything you need
  MyAttachedData.paletteColors = MyCppPaletteColorsModel {
  }

  // It can also work if you c++ model is a singleton
  MyAttachedData.paletteColors = MyCppSingletonPaletteColorsModel 

  // Or a context property, which you should avoid
  MyAttachedData.paletteColors = myCppContextPaletteColorsModel 

  // Usage
  Rectangle {
    anchors.fill: parent
    color: MyAttachedData.paletteColors.testData
  }
}

I didn't find much talk or discussion about attached properties, and yet I feel it's a really nice way to fix all the problems and it feel very qmlish. Have you used any attached properties in production yet?

Btw really nice talk: https://www.youtube.com/watch?v=yYCYPcbgXSs

Have a nice day.

Furkanzmc commented 1 year ago

To bring up the subject about the use of singleton, I now think that every use case where we wanted to use a singleton should now use attached properties, especially with new QQuickAttachedPropertyPropagator that standardize it and make it easy to use.

So I think CI-2: Use Singleton for Common API Access can be replaced with CI-2: Use Attached properties for Common API Access. I think that the Qt example with Theme is really self explainatory.

I think it can also be a good fit in CI-3: Prefer Instantiated Types Over Singletons For Data. One could imagine following use case:

You are right. I think attached properties are a more convenient way of extending a type or providing common data. Although, I wouldn't want to use it for API access. I think attached properties are great for data access and customization. Calling functions on them doesn't feel right.

I didn't find much talk or discussion about attached properties, and yet I feel it's a really nice way to fix all the problems and it feel very qmlish. Have you used any attached properties in production yet?

You are right. I'll put something together for this. Thank you for the feedback!

Btw really nice talk: https://www.youtube.com/watch?v=yYCYPcbgXSs

Thank you!