Moving from C++/Qt to Dart/Flutter

A Strategic Approach to Expanding Your Cross-Platform Toolkit

As a C++ developer using Qt, you've enjoyed the power of building cross-platform applications with a rich set of tools and libraries. However, with the rise of mobile and web applications, you might be looking to expand your toolkit. Enter Dart and Flutter—a modern language and framework that offer a robust environment for developing high-performance, cross-platform applications.

To help you navigate a shift from your existing C++/Qt applications to the Flutter framework, we’ll dive into the key similarities between Qt and Flutter, highlight the new concepts introduced by Dart and Flutter, and provide a practical roadmap.

Why Choose Flutter?

  1. Single Codebase: Write once, deploy on mobile (iOS and Android), web and desktop.
  2. Rich Widget Library: A comprehensive collection of Material and Cupertino widgets, offering modern and polished UI components.
  3. Hot Reload: Instantly see the effect of your code changes without rebuilding the entire app, which is a game-changer for productivity.
  4. Simplified UI Structure: Flutter uses a declarative, widget-based approach, simplifying the process of building complex UIs. The framework efficiently handles UI rendering based on the app's current state.

Flutter also offers additional capabilities worth noting, including:

Animations
Flutter provides full support for both Lottie and Rive animations, making it ideal for apps with high-quality, scalable animations. The Lottie and Rive packages are easy to integrate and well-documented, ensuring a smooth development process. With GPU-accelerated rendering, Flutter ensures smooth playback even for complex animations. These plugins work seamlessly across Android, iOS, web and desktop platforms, offering true cross-platform compatibility for your animations.

Moving from Qt Profiler to DevTools
Flutter DevTools offers an intuitive and integrated platform for profiling your app, inspecting widget trees, analyzing performance, and debugging UI interactions—all in real time.

IOT Communication
Flutter offers robust libraries for Bluetooth and WiFi communication, enabling seamless integration for IoT and connected devices. Packages like flutter_blue_plus and wifi_iot make it easy to manage Bluetooth connections and control WiFi settings across platforms, with a unified codebase for Android, iOS and more.

Testing
Flutter offers a comprehensive and easy-to-use testing framework that covers unit testing, widget testing, and integration testing all within a single environment. The test, flutter_test, and integration_test packages allow developers to write automated tests for both logic and UI components, ensuring that applications work as expected across different platforms.

Benefits of the Flutter Package Ecosystem

  1. Community and Package Ecosystem: The Flutter community provides numerous packages for everything from state management, animations and more. Popular packages like provider, bloc, and rxdart simplify complex tasks and speed up development.
  2. Simplified UI/UX Development: Flutter’s declarative nature allows for cleaner and more maintainable UI code, making complex layouts easier to implement.

Dart vs. C++/QML: Quick Comparison

  1. Language: Dart is a modern, object-oriented language with a syntax that feels familiar to developers accustomed to C++.
  2. Event Handling: Dart handles asynchronous events using async/await and Streams, replacing the signals/slots mechanism in Qt.
  3. Memory Management: Dart has a garbage collector, so you don't need to manage memory manually.
  4. No Pointers: Unlike C++, Dart does not have pointers, which simplifies memory safety.

From Signal/Slot to Reactive UI

The Fundamental Shift for C++/Qt Developers Transitioning to Flutter

For C++/Qt developers, the most dramatic shift when transitioning to Flutter is moving from the signal/slot mechanism to Flutter's reactive UI pattern. Instead of manually propagating changes and ensuring the UI updates correctly, Flutter treats the UI as a function of state (see Declarative UI Approach:

Declarative state


In this model, the developer provides a single, declarative mapping from application state to user interface. The Flutter framework then takes care of updating the interface automatically when the application state changes. This approach eliminates the need to explicitly manage update paths, reducing the complexity of cascading state changes and synchronization bugs.

Traditional Challenges and Flutter's Solution

In traditional frameworks like Qt, developers must:

  • Define the UI's initial state.
  • Write separate code to update the UI when state changes.

This dual-path maintenance can lead to inconsistencies, especially in complex applications where a minor change in one part of the UI may unintentionally affect unrelated areas. Solutions like MVC try to manage these complexities but often struggle with synchronization between initialization and update steps.

Flutter avoids these pitfalls by embracing reactive programming, which decouples the UI from its underlying state. Developers focus on describing the UI as a reactive mapping of application state through widgets, which are:

  • Immutable: They serve as configuration objects for rendering the UI.
  • Efficient: Flutter's framework optimizes updates, ensuring only the necessary parts of the UI are redrawn.

How Flutter Enhances Development Productivity

Flutter’s reactive model eliminates the traditional hassle of maintaining dual code paths (initialization and update). Developers define the UI once, and the framework handles both creation and updates seamlessly. This dramatically improves development efficiency, reduces bugs, and simplifies reasoning about the application state.

Familiar Territory: Mapping Qt Concepts to Flutter

Transitioning to Flutter becomes easier when you can relate its components to those you already know from Qt.

Building UIs with Widgets: Flutter vs. Qt

In Qt, widgets are the core of your UI development. Flutter shares this fundamental idea, with widgets serving as the essential building blocks.

Imagine your app’s user interface as a house made of LEGO bricks. Each widget in Flutter is like a single LEGO piece, a fundamental building block that helps shape your UI. You can mix and match these widgets to construct complex and engaging designs. Because Flutter is declarative, it allows developers to easily specify how the UI should look based on the app’s current state.

  • Qt: QWidget is the base class for all UI components. Examples include: an QPushButton, QLabel, QLineEdit, etc.
  • Flutter: The Widget class serves a similar purpose, with StatelessWidget, StatefulWidget and InheritedWidget as the primary building blocks. Common examples include: ElevatedButton,Text, TextField, etc.

Understanding Widget Trees in Flutter

Every widget contains a build() method. In this method, you create a UI composition by nesting widgets within other widgets. This forms a tree-like data structure. Each widget can contain other widgets, commonly called children. Here is visualization of a widget tree:

Architecture View

Image referenced from https://docs.flutter.dev/resources/architectural-overview

Note: Flutter’s framework actually manages not one, but three trees in parallel: Widget tree, Element tree and RenderObject tree. Here’s how a single widget works under the hood.

  • Widget: The public API or blueprint for the framework.
  • Element: Manages a widget and a widget’s render object.
  • RenderObject: Responsible for drawing and laying out a specific widget instance. Also handles user interactions, like hit testing and gestures.
Trees


Image referenced from https://docs.flutter.dev/resources/architectural-overview

Understanding Flutter Widget Types: Stateless, Stateful, and Inherited

There are three major types of widgets: StatelessWidget, StatefulWidget and InheritedWidget. All widgets are immutable but some have state attached to them using their element.

  • Stateless widgets: A widget that does not require mutable state. You can’t alter the state or properties of Stateless widgets once it’s built. When your properties don’t need to change over time, it’s good practice to start with a stateless widget.
  • Stateful widgets: A widget that has mutable state. Stateful widgets preserve state, which is useful when part of the UI you are describing needs to change dynamically.
  • Inherited widgets: Base class for widgets that efficiently propagate information down the tree. Lets you access state information from the parent elements in the tree hierarchy.

QObject and Inheritance: Qt vs. Flutter

  • Qt: QObject as the Foundation

    In Qt, QObject serves as the base class for all objects that leverage the signals and slots mechanism. This mechanism facilitates communication between components, enabling event-driven and loosely coupled architectures.

  • Flutter: Modular Inheritance

    Flutter does not have a single base class equivalent to QObject. Instead, it adopts a modular approach where foundational functionality is distributed across various classes. For widget-based inheritance:

  • Widget (or its subclasses like StatefulWidget and StatelessWidget) forms the root of the widget hierarchy.
  • To propagate data efficiently through the widget tree, Flutter uses InheritedWidget, which provides context and state sharing across child widgets.

Signals and Slots vs. Streams: Communication in Qt and Flutter

Qt uses the signals and slots mechanism for communication between objects, enabling a loose coupling between UI components and logic. Flutter achieves similar functionality through asynchronous programming.

  • Qt: Signals and slots enable event-driven programming.
  • Flutter: Asynchronous programming is handled using Streams and Future, making it easier to work with data and events.

Declarative UI Approach

If you've worked with Qt Quick (QML), you know the power of building UIs declaratively. Flutter embraces this approach, allowing you to construct UIs by describing how they should look in a declarative style.

Qt Quick Example:
Qml

Rectangle {
    width: 200; height: 200
    Text {
        text: "Hello, World!"
        anchors.centerIn: parent
    }
}

Flutter Examples:
Dart

Container(
  width: 200,
  height: 200,
  child: Center(
    child: Text('Hello, World!'),
  ),
                                                                                                                                                                                                                                                                                                                )
return ViewB(
  color: red,
  child: const ViewC(),
);

State Management is Key to Flutter Development

State is when a widget is active and stores its data in memory. The Flutter framework handles some state but as mentioned above, Flutter is declarative. This means that Flutter builds its user interface to reflect the current state of your app.

Declarative state

Two state types to consider are ephemeral state, also known as (UI state or local state) and app state.

  • Use Ephermeal state when no other component in the widget tree needs to access a widget’s data. Here are a few examples.
    • current page in a PageView
    • current progress of a complex animation
    • current selected tab in a BottomNavigationBar

Implementation: Ephemeral state is often managed using setState(), which is simple but limited to single widgets.

  • Use App State when other parts of your app need to access a widget’s state data. Examples of application state:
    • User preferences
    • Login info
    • Notifications in a social networking app
    • The shopping cart in an e-commerce app
    • Read/unread state of articles in a news app

Implementation: Application state is better managed with structured approaches like Provider, Bloc, or Riverpod.

When to Choose a State Management Approach

CriterionEphemeral State (setState)Application State (Provider/Bloc/Riverpod)
Scope of StateConfined to a single widget.Shared across multiple widgets or screens.
App ComplexitySimple apps with limited interactivity.Complex apps with interconnected features.
Performance ConcernsMinimal, as only one widget is updated.Scales better but requires careful architecture.
TestabilityBasic UI testing suffices.Structured testing of state transitions is essential.

Porting Your Application: From C++/Qt to Flutter

Transitioning your application from C++/Qt to Flutter involves several steps. Here's a roadmap to guide you through the process.

1. Isolate the UI Layer

Begin by decoupling your application's UI from its logic. This separation makes it easier to port the UI without affecting the core functionality.

  • Identify UI Components: List all the widgets and dialogs used in your Qt application. Make sure you understand how they are arranged and laid out.
  • Abstract Business Logic: Ensure that your core logic can operate independently of the UI. This is often one of the biggest challenges of the entire effort.

2. Recreate the UI in Flutter

Start building your application's UI in Flutter, leveraging its rich set of widgets.

  • Map Qt Widgets to Flutter Widgets: Find equivalent widgets or create custom ones if necessary.
  • Apply Layouts: Use Flutter's layout widgets like Row, Column, and Stack to arrange your UI elements.

3. Connect Existing Application Logic

Integrating your existing C++ logic into a Flutter app involves multiple approaches. While this section focuses on Dart's FFI (Foreign Function Interface), it's essential to acknowledge that FFI is not always the most efficient or practical choice. Alternatives such as Platform Channels and gRPC provide additional flexibility and should not be overlooked.

Option A: Use Dart's FFI (Foreign Function Interface)

Dart's FFI allows you to call native C/C++ code directly from Dart. This method is suitable for lightweight, computationally intensive tasks or when you need low-level access to existing C++ libraries.

  • Expose C++ Functions: Create a C-compatible API for your core logic. The FFI library can only bind against C symbols, so in C++, these symbols must be marked extern "C".
  • Call Native Functions: Use dart:ffi to load dynamic libraries and interact with the exposed functions.

Steps to Use FFI:

1. Open the Dynamic Library

// Load the dynamic library using DynamicLibrary.open:
final dylib = ffi.DynamicLibrary.open('libnative.so');

2. Create a Typedef for the FFI Type Signature of the C Function

// Define the FFI type signature for the C function. For example, // if the function returns void and takes no parameters:
typedef hello_world_func = ffi.Void Function(); 

3. Create a Typedef for the Dart Representation of the Function

//Define a typedef for the Dart function type:
typedef HelloWorld = void Function();

4. Lookup and Call the Native Function

// Get a reference to the C function and convert it to a Dart function:
final HelloWorld hello = dylib
    .lookup<ffi.NativeFunction<hello_world_func>>('hello_world') .asFunction();

5. Call the Function

hello();

Option B: Use Platform Channels

For most applications requiring integration with C++, Platform Channels are a more practical alternative. Platform Channels allow Dart code to communicate with platform-specific native code (e.g., C++ via JNI on Android or Objective-C++ on iOS). This approach is ideal when your C++ logic interacts with OS-level features or requires platform-specific APIs.

  • Flutter to Native Communication: Dart communicates with platform-specific code using method channels.
  • Leverage JNI (Android) or Objective-C++ (iOS): Integrate your C++ logic on each platform using platform-native tools.
  • Shared Logic: Centralize shared logic in your C++ codebase while bridging communication using Platform Channels.

Option C: Use gRPC for Decoupled Integration

If your C++ logic can be abstracted as a backend service, consider exposing it via gRPC. Flutter's grpc package allows Dart to communicate with a gRPC server, enabling seamless interaction with your C++ logic.

  • Advantages:
    • Decouples UI from backend logic.
    • Enables reuse of the C++ logic across multiple platforms or clients.
    • Facilitates scalability and remote execution.
  • Use Case: When your C++ logic serves multiple applications or when real-time communication is required.

Option D: Port Logic to Dart

When feasible, rewriting your core logic in Dart is often the most maintainable solution for Flutter applications. This option leverages Dart's ecosystem and avoids the overhead of native integration.

  • Translate Classes and Methods: Convert your C++ classes to Dart.
  • Replace Dependencies: Use Dart packages to replace or enhance functionality previously reliant on C++ libraries.

FFI is a powerful tool for integrating native C++ code, it's not always the best choice for porting existing application logic. Platform Channels offer a better fit for platform-specific integration, and gRPC excels in decoupled, networked use cases. For long-term maintainability, porting logic to Dart should also be considered when feasible. By carefully evaluating the needs of your application, you can choose the integration method that best aligns with your goals.

4. Implement State Management

For managing app state, you'll want to research your options. Your choice depends on the complexity and nature of your app, there is no clear-cut rule. Below are examples of key state management concepts.

Criteria for Choosing a State Management Solution

  • Complexity of the App:
    • Simple Apps: Apps with minimal interaction or straightforward logic (e.g., displaying static content or simple forms) can use Flutter’s built-in setState() to update UI components.
    • Complex Apps: Apps with multiple interconnected components, asynchronous data streams, or advanced features like real-time updates or multiple screens benefit from a more robust state management solution. Consider using providers like Provider, Bloc, or Riverpod.
  • Asynchronous Data Handling:
    • If your app deals with streams of data (e.g., fetching data from APIs or listening to real-time updates), a solution like Bloc, which integrates well with streams, is ideal.
  • Scope of State:
    • Local State: When state is specific to a single widget or a small part of the widget tree, setState() or InheritedWidget is sufficient.
    • Global State: If you need to share state across multiple, unrelated widgets (e.g., user authentication status, app themes, or settings), libraries like Provider, Bloc, or RiverPod are better suited.
  • Testability and Scalability:
    • Apps expected to scale over time or require robust testing should favor structured state management solutions like Bloc or Riverpod, which enforce clear separation of concerns.

Choosing the Right State Management Solution

Your choice depends on the complexity and requirements of your app:

CriterionSimple AppsComplex Apps
State ScopeUse setState() or InheritedWidget for local or ephemeral state.Use Provider, Bloc, or Riverpod for global or app-wide state.
Asynchronous DataSimple async calls using FutureBuilder or StreamBuilder.Use Bloc for handling complex data streams, like WebSocket connections or real-time updates.
TestabilityTest widgets independently, focusing on UI behavior.Use Bloc or Riverpod for structured, testable state transitions.
Team DynamicsProvider for small teams or newcomers to Flutter.Bloc or Riverpod for experienced teams working on large-scale or long-term projects.
1. Simple State Management with setState()

For apps with minimal interactions or straightforward logic, Flutter’s built-in setState() is sufficient.

import 'package:flutter/material.dart';
class CounterApp extends StatefulWidget {
  @override
  _CounterAppState createState() => _CounterAppState();
}
class _CounterAppState extends State<CounterApp> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter App')),
      body: Center(
        child: Text('Counter: $_counter', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        child: Icon(Icons.add),
      ),
    );
  }
}

Use Case: Localized and simple state, such as incrementing a counter or toggling a UI element.

2. Managing Complex State with Provider

For apps with complex and interconnected components, the Provider package offers an efficient solution.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CounterNotifier extends ChangeNotifier {
  int _counter = 0;
  int get counter => _counter;
  void increment() {
    _counter++;
    notifyListeners();
  }
}
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CounterNotifier(),
      child: CounterApp(),
    ),
  );
}
class CounterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: CounterScreen(),
    );
  }
}
class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterNotifier = Provider.of<CounterNotifier>(context);
    return Scaffold(
      appBar: AppBar(title: Text('Provider Counter')),
      body: Center(
        child: Text('Counter: ${counterNotifier.counter}', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: counterNotifier.increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

Use Case: Medium-to-large apps where state needs to be shared across multiple screens or components.

4. Advanced State Management with Bloc

The Bloc (Business Logic Component) pattern is ideal for complex applications with real-time data updates and asynchronous streams.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
  void increment() => emit(state + 1);
}
void main() {
  runApp(
    BlocProvider(
      create: (context) => CounterCubit(),
      child: CounterApp(),
    ),
  );
}
class CounterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: CounterScreen(),
    );
  }
}
class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterCubit = context.read<CounterCubit>();
    return Scaffold(
      appBar: AppBar(title: Text('Bloc Counter')),
      body: Center(
        child: BlocBuilder<CounterCubit, int>(
          builder: (context, state) {
            return Text('Counter: $state', style: TextStyle(fontSize: 24));
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: counterCubit.increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

Use Case: Large-scale, asynchronous apps that require robust and testable state management.

2. Testing and Optimization
  • Test Functionality: Ensure that the ported application behaves as expected.
  • Optimize Performance: Profile your application and optimize resource-intensive operations.

Transition to Flutter to Level Up Your Cross-Platform Skills

Transitioning from C++/Qt to Dart/Flutter opens up new possibilities for cross-platform development, especially in the mobile and web domains. While the languages and frameworks have their differences, many core concepts remain familiar. By leveraging your existing knowledge and embracing the new paradigms introduced by Dart and Flutter, you can expand your skill set and take advantage of a modern, reactive framework that enhances productivity and performance.

Ready to create native-like applications, from mobile to embedded, more quickly and with lower development costs? ICS offers a range of Flutter services, including development services and tutorials, to help you create impactful applications that meet user expectations and drive business success. For help unlocking the full potential of this powerful development environment, reach out to the Flutter experts at ICS.