QML Best Practices

QML Best Practices


In short

QML best practices to write clean, maintainable, and high-performance code with Qt Quick

Avoid common QML pitfalls (bindings, business logic in the UI)
Organize files and folders with clear project structures
Apply a consistent order for properties, signals, and functions
Decouple the UI from business logic for better maintainability
Design components with simple, reusable APIs
14 min
Intermediate

Table of Contents

In this article, I share with you some QML best practices that will help you avoid classic pitfalls and build interfaces that are clear, responsive, and easy to evolve.

Best practices in development: the secret of projects that last

How many times have you heard (or thought): “This code is a mess, we should rewrite everything!”

Whether you code in QML, Python, JavaScript or anything else, this phrase is a symptom: somewhere, best practices haven’t been applied. Or worse, they’ve been ignored in favor of speed.

In this article, I share with you what I’ve learned (sometimes the hard way) about best practices, conventions and pitfalls to avoid.


What is a good practice?

For me, it’s a habit or rule that brings tangible benefit:

  • Visible: Fewer bugs, faster deliveries.
  • Invisible (but crucial): More readable code, calmer team, project easier to evolve.

Example: Clear variable naming, separation of responsibilities, order of property declarations… Details? Yes. But it’s like oil in an engine: without it, everything creaks.

⚠️ Warning: Some good practices require initial effort. A junior might find them “useless”, a senior will see it as time saved in the long run. This is normal: their value reveals itself with experience.


Convention: the art of agreement

A convention is an arbitrary but shared rule. It doesn’t matter whether you use tabs or spaces, the important thing is that the whole team does the same.

Why?

  • Consistency: Everyone speaks the same language.
  • Efficiency: Less time wasted deciphering everyone’s style.

My advice: Document your conventions (a CONTRIBUTING.md or wiki is enough). It avoids endless debates and Merge/Pull Requests blocked over details.


Bad practices: the trap of “it works, so it’s good”

Bad practices are like technical debt: we contract them without thinking about it, and one day, they blow up in our face.

Some telltale signs:

  • “We don’t have time to refactor, we’ll see later.” (Spoiler: that day never comes.)
  • “It’s simpler to copy-paste this code than generalize it.” (Until you have 10 versions of the same piece of logic.)
  • “It compiles, so it’s good.” (Until a bug hides in a corner and ruins your Friday evening.)
  • “QML files of 500+ lines, because it’s simpler to put everything in the same place.” (Spoiler: no, it’s not simpler when you need to debug or add a feature.)

My advice: Take the time to do things right, even if it’s longer at first. Your future self will thank you.


Who is this article for?

  • Beginners: To start on good foundations.
  • Experienced developers: To revisit your habits.
  • Teams: To align everyone and avoid frustrations.

And now, let’s get practical! We’re now going to see how to apply these principles concretely in QML (organization, readability, maintainability…). But first, a question: What’s the worst bad practice you’ve seen (or committed)? 😉 Feel free to respond in the comments of this article.

My nightmare? A project where all files were piled up at the root – no folders, no structure, just 200 or 300 C++ files lost in the same folder. Result: a daily treasure hunt to find a class. The worst part? The team seniors found this normal after 15 years of development, and the junior interns believed hard as iron that this was the standard way to organize a project. In short, developer hell.


Why does QML deserve special attention?

QML is a declarative and flexible language, ideal for creating fluid and dynamic interfaces. But this freedom comes with a price: without rigor, you quickly end up with spaghetti code, where the visual hierarchy no longer reflects the logical structure, and where bindings (property: value) become sneaky traps.

Why is QML different? QML has a particularity: it allows the possibility to merge interface and logic (thanks to integrated JavaScript). Without vigilance, this flexibility turns against you:

  • Code structure must match visual hierarchy – otherwise, you end up with components impossible to reuse or test.
  • Poorly managed bindings (like a width: parent.width * someComplexFormula) make application behavior unpredictable and hard to debug.
  • Business logic slips into the UI – a Button that calculates a price or validates a form is tempting… but it’s also opening the door to bugs and technical debt.

Consequence? Code where everything depends on everything, where a minor modification breaks three screens, and where nobody dares touch anything anymore.

Fortunately, a few simple rules are enough to avoid chaos. Let’s review them, section by section.


Code organization

Where to start? With organization, of course! A clear project structure is like a good foundation: if it’s solid, everything else follows. Otherwise, prepare for sleepless nights of refactoring.

How to organize your project directory?

In a professional project mixing C++ and QML, you often end up with a ton of files: .h, .cpp, .qml, .js, .png, .svg, .ttf, CMakeLists.txt, etc. The challenge? Finding a balance between rigor and flexibility, as there’s no universal solution. Here are two examples of structures I’ve experimented with, along with their strengths and limitations.


Basic structure (simple project)

project_name/
├── include/               # C++ headers
│   └── project_name/
│       └── cpp_folder/
│           └── MyClass.h
├── resources/             # Static resources (images, fonts, etc.)
│   ├── images/
│   └── fonts/
├── src/
│   ├── main.cpp           # C++ entry point
│   ├── cpp_folder/        # C++ implementations
│   │   └── MyClass.cpp
│   └── qml/               # QML code
│       └── qml_folder/
│           └── Component.qml
├── test/                  # Unit tests
│   └── cpp_folder/
│       └── MyClassTest.h
├── .clang-format          # Automatic formatting
├── .clang-tidy            # Static analysis
├── .gitignore
└── CMakeLists.txt         # Build configuration

Why does this work?

  • Clear separation between C++ and QML.
  • Centralized resources in a dedicated folder.
  • Isolated tests to avoid conflicts.

Advanced structure (complex project)

project_name/
├── app/                 # Main application (same structure as above)
|
├── core_library/        # C++ library (business + technical, under Git repo)
|
├── ui_library/          # QML library (reusable graphical components, under Git repo)
│   ├── token/           # Design tokens (colors, fonts, sizes)
│   ├── atom/            # Atomic components (buttons, text fields)
│   ├── molecule/        # Composite components (section + title + button)
│   ├── organism/        # Complex components (headers, footers)
│   └── resources/       # UI-specific resources
|
├── modules/             # Independent modules (each module = a Git sub-project)
│   ├── module1/
│   └── module2/
|
├── .clang-format
├── .clang-tidy
├── .gitignore
├── .gitmodules          # For Git submodules
└── CMakeLists.txt

Why this structure?

  • Modularity: Each part of the project is autonomous and can evolve independently.
  • Reusability: The core_library and ui_library libraries are autonomous and can be shared between multiple projects.
  • Scalability: Adding a new module or feature doesn’t break existing code.

To remember:

  • Adapt the structure to your team and constraints.
  • Document your choices! No matter where, the main thing is that everyone can access them easily (README.md, Confluence, Notion, etc.).
  • Avoid “catch-all” folders like misc/ or temp/ – they quickly become digital garbage bins.

How to organize your QML files and folders?

I present here two approaches to organize your QML files. Feel free to mix them according to your needs!

Functional organization (by feature)

Ideal if your application is business-oriented (ex.: a deals app).

project_name/
└── qml/
    ├── header/                         # Application header
    │   ├── Header.qml
    │   └── SettingsPanel.qml
    ├── dashboard/                      # Dashboard
    │   ├── Dashboard.qml
    │   └── MyGoodDeals.qml
    ├── menu/                           # Navigation menu
    │   ├── Menu.qml
    │   └── MenuItem.qml
    ├── feature/                        # Business features
    │   ├── profil/
    │   │   ├── ProfilPage.qml
    │   │   └── ProfilResumePanel.qml
    │   └── deal/
    │       ├── DealPage.qml
    │       └── LastDealListPanel.qml
    └── Main.qml                        # QML entry point

Why?

  • Intuitive: Developers easily find files related to a feature.
  • Consistent with business: The structure reflects the application’s organization.

Organization by component category

Ideal if you use a design system or component library.

project_name/
└── qml/
    ├── token/                          # Application style and base variables
    │   ├── Fonts.qml
    │   ├── Icons.qml
    ├── atom/                           # Simple components (atomic)
    │   ├── Button.qml
    │   ├── Divider.qml
    │   ├── SpecificButton.qml
    ├── molecule/                       # Complex components (composed of atoms)
    │   ├── ProfilPanel.qml
    │   └── DealCard.qml
    ├── organism/                       # Entire panels (composed of molecules / atoms)
    │   ├── Header.qml
    │   └── Menu.qml
    └── page/                           # Complete pages
        └── Dashboard.qml

Why?

  • Reusability: Components are decoupled and easy to maintain.
  • Compatibility with design systems (like Atomic Design).

My advice:

  • Mix both approaches if necessary (ex.: category organization for components, functional for business functions).
  • Be consistent: Once the structure is chosen, apply it everywhere.
  • And above all, adapt the approach to your needs!

Readability

Readability is like spelling: everyone knows it’s important, but few people make it a priority. Yet readable code is maintainable, collaborative and less stressful code.

I won’t rehash the good practices you already know by heart:

  • “Give clear names to your variables, classes and components.” (Yes, userProfileAvatar > img1.)
  • “In a comment, explain the why, not the how.” (The code is there for that.)

No, let’s focus now on what is specific to QML.


Declaration order of elements in QML

In QML, declaration order matters. Not for the compiler, but for your colleagues (and your future self). Here’s the order I recommend, tested and approved on real projects:

  1. id (always first)
  2. Properties (property declarations)
  3. Inherited properties (ex.: anchors, width, height)
  4. Signals
  5. JavaScript functions
  6. Private elements (declarations we want to keep internal)
  7. Child objects (nested components)
  8. States (states)
  9. Transitions (transitions)

My QML template

copy-paste without moderation

import QtQuick     // Qt imports
import my.project  // Internal plugin or module
import "my/folder" // Relative import

Item {
    id: root  // Always first!

    //========================================================================
    // Properties
    //========================================================================
    property bool isActive      // false by default, so no re-assignment
    property string userName    // "" by default, so no re-assignment

    //========================================================================
    // Inherited properties
    //========================================================================
    anchors.fill: parent
    width: 200
    height: childrenRect.height

    //========================================================================
    // Signals
    //========================================================================
    signal userUpdated(string newName)

    //========================================================================
    // Functions
    //========================================================================
    function calculateWidth() {
        return root.width * 0.8
    }

    //========================================================================
    // Private elements
    //========================================================================
    
    QtObject {
        id: internal

        property string userPwd
    }

    //========================================================================
    // Child objects
    //========================================================================
    Rectangle {
        color: "lightgray"
        anchors.fill: parent
    }

    //========================================================================
    // States
    //========================================================================
    states: [
        State {
            name: "active"
            when: root.isActive
            PropertyChanges { target: root; color: "blue" }
        }
    ]

    //========================================================================
    // Transitions
    //========================================================================
    transitions: [
        Transition {
            from: "*"
            to: "active"
            ColorAnimation { duration: 200 }
        }
    ]
}

Why this order?

  • Logical: We go from the most global (the component) to the most specific (children, states, transitions).
  • User-oriented: We present first the public API of the component with its properties
  • Readable: Sections are visually separated by comments.
  • Maintainable: Adding or modifying a section doesn’t break readability.

Little bonus:

  • Use visual comments (// ====) to delimit sections.
  • Align properties for a clean rendering (ex.: property bool isActive: false).

Maintainability

Maintainability is the art of not shooting yourself in the foot in 6 months. Maintainable code is code where:

  • Each component has a single responsibility.
  • Business logic is decoupled from the interface.
  • Dependencies are minimal and clear.

One entity, one responsibility

A fundamental principle: each element of your code – whether it’s a QML component, a C++ class, or any other entity – must have a single responsibility. This means it must respond to a precise and unique need.

Why is this so important? How many times have we seen overloaded components or classes, where functions with multiple responsibilities pile up? This mixing of responsibilities makes code difficult to maintain, test and evolve. A QML component must do one thing, and one thing only.

Separation of responsibilities: UI vs. Business logic

It’s crucial to emphasize that business logic should never be managed by QML. The role of QML is to present the user interface and react to user interactions, but it must remain decoupled from business logic.

This separation is actually a pillar of modern software architectures, such as:

  • Clean Architecture: In this model, the user interface (UI) is considered an external entity, just like a database. It’s isolated from the business core to guarantee maximum independence.
  • QML Core UI Architecture: An approach that promotes strict decoupling between the UI layer and business logic.

Visual example: In the diagram below, from Clean Architecture, the UI layer (IHM) is clearly positioned as an external plugin, distinct from the business domain.

Clean Architecture


Why adopt this approach?

  • Maintainability: Decoupled code is easier to fix and evolve.
  • Testability: Business logic can be tested independently of the UI…and vice versa.
  • Reusability: UI or business components can be reused in other contexts.

In summary: Respect the principle “one entity, one responsibility”, and keep your QML exclusively dedicated to the interface. Business logic has its place elsewhere.

Ultimately, the golden rule (which applies to most languages):

“One QML component = one responsibility. Period.”

Concrete example:

  • To avoid:
    // ProfilePage.qml (500 lines)
    Item {
        // 1. Displays user profile
        // 2. Manages API connection
        // 3. Validates form fields
        // 4. Sends notifications
        // ...
    }
    
  • To do:
    // ProfilePage.qml (50 lines)
    Item {
        ProfileHeader { user: userManager.currentUser() }
        ProfileForm { onSubmit: userManager.save() }
        NotificationPopup { }
    }
    
    Business logic? It’s in userManager (C++).

Why does this work?

  • Decoupling: Each component is autonomous and testable.
  • Reusability: ProfileHeader can be used elsewhere.
  • Maintainability: Modifying business logic doesn’t break the UI.

The communication interface

A good component is like a black box: you only see what’s necessary to use it, the rest stays hidden. But to achieve this, it’s essential to think about inputs/outputs, what should be public and what should stay private before even writing a line of code.

The key? Adopt a user-centered approach for your component (whether that’s you in 6 months or a colleague). Ask yourself:

  • How would I like to use this component ideally?
  • What information do I need to provide for it to work?
  • What should it return or indicate to me?

My little ritual (inspired by TDD): Before coding, I often write pseudo-code imagining my component already finished. For example:

// Pseudo-code: How would I like to use my component?
UserProfile {
    user: backend.currentUser  // Clear input
    editable: isAdmin         // Simple option

    onSaved: showSuccess()    // Explicit output
}

Then, I work to make this pseudo-code become reality. This method forces me to:

  • Think in terms of real needs (and not just “make it work”).
  • Avoid catch-all components that do everything and anything.
  • Make the API intuitive from conception.

Result? A component simpler to use, test and maintain – and above all, that really responds to the needs of those who will use it.


Concrete example (taken and improved):

// UserProfile.qml - API designed "backwards"
Item {
    id: root

    // ===== PUBLIC API (what the user looks at) =====

    //========================================================================
    // Properties
    //========================================================================
    required property User user       // User data (input)
    required property bool editable   // Edit mode (input)

    //========================================================================
    // Signals
    //========================================================================
    signal saved()         // Save event (output)
    signal error(string)   // Error handling (output)

    
    // ===== END PUBLIC API =====

    // ===== IMPLEMENTATION (inside the black box) =====

    //========================================================================
    // Private elements
    //========================================================================
    QtObject {
        id: internal

        function validate() 
        { 
            /* Internal logic */ 
        }
    }

    //========================================================================
    // Child objects
    //========================================================================
    Column {
        TextField { text: root.user.name; enabled: root.editable }
        Button {
            text: "Save"
            onClicked: {
                if (internal.validate()) 
                {
                    root.saved(root.user?.name ?? "");
                }
                else 
                {
                    root.error("Invalid fields");
                }
            }
        }
    }
}

Why does this approach change everything?

  • Clarity: The API is designed to be used, not just to “work”.
  • Sustainability: Less painful refactoring later.
  • Collaboration: Your colleagues (or yourself in the future) will thank you.

In summary:

  1. Decouple business logic from UI.
  2. Imagine ideal usage before coding (pseudo-code > implementation).
  3. Clearly separate your public/private APIs.
  4. Test the API in real conditions from the start.

Conclusion

Best practices aren’t luxury or purist theory: they’re what makes a project stay alive and pleasant to maintain over time.
In QML as elsewhere, clear, decoupled and readable code means fewer bugs, more serenity and a team that moves forward without being afraid!

If you’ve made it this far, you’ve already taken a big step: awareness.
What’s next? Daily application – and that’s often where it gets difficult. Because between deadlines, technical debt and project reality, it’s easy to fall back into the traps I’ve described.

💡 To deepen the subject, I highly recommend this GitHub repository that gathers many tips and best practices in QML. You’ll find both style rules (naming, indentation, file organization) and design recommendations (view structuring, proper use of properties, performance optimization, etc.).

👉 QML Coding Guide – Furkanzmc


What if we talked about it for your project?

As a freelance developer and architect specialized in C++ / Qt / QML, I help my clients to:

  • design solid and scalable architectures,
  • implement best practices adapted to their context,
  • train teams on QtQuick and QML specificities,
  • and deliver robust, maintainable applications that truly meet end-user needs.

👉 If you want to apply these principles to your project (or simply avoid it turning into spaghetti code 😉), I’d be happy to discuss it with you.

📩 Contact me
Share :
comments powered by Disqus