4. Chapter 3 - Introduction to Flutter
4.1. Sources
Official documentation: https://docs.flutter.dev—in particular the pages “What is Flutter?”, “Introduction to widgets,” and “State management.” [Dart] language: https://dart.dev/language
4.2. What Flutter Is Not
This is the most important starting point for this chapter, as it differs significantly from the three previous versions of this course. [Angular], [React], and [Vue.js] all produce [JavaScript]/[TypeScript] COMPILED, which is executed by the [JavaScript] engine already present in a browser—this engine handles all the display work, via [DOM] and [CSS]. A tool like [Cordova]/[Capacitor] (not used in this course) does allow you to package such a web client into a mobile shell, but the client then continues to run in an embedded browser (a [WebView])—never natively.
[Flutter] has nothing to do with any of this. The [Dart] code in this document is compiled, for both mobile and desktop, into NATIVE machine code (compilation [AOT], [Ahead-Of-Time] —the same compilation family as a C++ or Rust program), which is directly executable by the target operating system; it requires NO [JavaScript] interpreter or [WebView] of any kind. [Flutter] includes its own graphics rendering engine ([Impeller], which replaced [Skia] as the default engine on mobile devices): the application draws each of its pixels itself, frame by frame, rather than delegating the display to the operating system’s native widgets (unlike [React Native], which is not used in this course and which instead drives the actual interface components of [Android]/[iOS]). It is this architectural choice that allows [Flutter] to render an identical interface across the three environments targeted by this document - there are no native operating system components whose appearance might vary from one platform to another.
On the [web] target, however, there is obviously no compilation to native machine code: [flutter build web] compiles the [Dart] code to [JavaScript] (or to [WebAssembly], a more recent target), which is executed by the browser—but this generated [JavaScript] does NOT manipulate the [DOM] or [HTML] the way [Vue.js] does: it, too, renders directly onto a single [<canvas>] element (or via automatically generated [HTML] elements, depending on the chosen rendering mode), thereby reproducing the same [Flutter] appearance as on mobile and desktop rather than relying on [HTML] elements styled in [CSS].
4.3. The Flutter development model: everything is a widget
[Angular], [React], and [Vue.js]—all three distinguish the LOGIC of a component ([TypeScript]) from its PRESENTATION (a [HTML] template, which may be separate or integrated depending on the framework—see Chapter 4 of the [Vue.js] document for this course, “Components: [.vue] files and [Composition API]”). [Flutter] completely eliminates this distinction: EVERYTHING, within a [Flutter] interface, is a [Widget]—a button, text, margin, alignment, and background color are each a widget, and a complete interface is nothing more than a TREE of widgets nested within one another, described by ordinary [Dart] code (the [build(BuildContext context)] method) rather than by a separate template language—in this respect, [Flutter] takes the choice already made by [React] ([JSX], from [JavaScript], which describes the interface) even further: whereas [JSX] remains a markup language—MIXED with [JavaScript]— [build(...)] simply returns a [Dart] EXPRESSION—nested constructor calls ([Scaffold(appBar: ..., body: Column(children: [...]))])—without any additional syntax to learn.
4.4. StatelessWidget and StatefulWidget: whereas Vue.js requires no distinction
[Vue.js] (like [React] and [Angular] before it in this course) makes no structural distinction between a component that has a mutable state and one that does not: [ref(...)] may or may not be called within any [<script setup>], without changing anything in the component’s declaration itself. [Flutter] IMPOSES this distinction right from the class declaration: a [StatelessWidget] (nothing ever changes after its construction—see [ScheduleView] and [LoginPage] in Chapter 4) re-runs its [build(...)] method ONLY when its parameters change; a [StatefulWidget] is paired with a separate [State<T>] class (see [_LoginFormState], [_AppRootState]..., Chapter 4), which holds the mutable state and the method [setState(() { ... })] —the EXPLICIT call that triggers a new rendering, whereas [reactive(...)] on the [Vue.js] side automatically detects every property write. This separation into two classes (Widget/State) is a direct result of a deliberate design choice in [Flutter]: a [Widget] object is IMMUTABLE (rebuilt on every render, like a simple configuration object), whereas its associated [State] SURVIVES from one render to the next.
4.5. Sharing state between widgets: provider, the equivalent of provide/inject
[Vue.js] shares state between remote components via the pair [provide]/[inject]; [React] uses a [Context]; [Angular] uses an injectable singleton service. [Flutter] does not have any of these mechanisms built into the framework itself: the third-party package [provider] (see [pubspec.yaml], Chapter 4), which is virtually indispensable in the [Flutter] ecosystem, serves this purpose. Its principle: a [ChangeNotifier] class (a standard [Dart] class, which can notify its listeners of a change via [notifyListeners()]) is made available to an entire subtree of widgets by a [ChangeNotifierProvider] placed at its root (see [MultiProvider] in [lib/main.dart], which is equivalent to the [app.provide(key, value)] calls on the [Vue.js] side); any descendant widget then re-reads it using [context.watch<MyService>()] (subscribes to future changes—the equivalent of [useXxx()] on the [Vue.js] side) or [context.read<MyService>()] (one-time read, without resubscription—a distinction that [Vue.js] does not need to make).
Key difference from [reactive(...)]: a [ChangeNotifier] does not automatically detect ANYTHING. [reactive(...)] wraps an object in a [JavaScript] proxy that intercepts every property READ and every property WRITE; a [ChangeNotifier] [Dart] is a regular class, and it is up to the developer to explicitly call [notifyListeners()] after every modification that should trigger a new rendering—requiring more manual discipline, in exchange for a mechanism that doesn’t rely on “magic” proxies to understand.
4.6. No separate template language: conditional rendering and lists
[v-if]/[v-else] and [v-for] ([Vue.js]) — such as [@if]/[@for] on the [Angular] side, or the ternary operators && and [.map(...)] on the [React] side - are converted, in [Flutter], back into ORDINARY [Dart] expressions, directly within [build(...)]: a classic [Dart] [if]/[else] for a condition, and [.map((element) => Widget(...)).toList()] for a list—the same concept as [.map(...)] on the [React] side, since [Flutter], like [React], does not recognize a separate “template language” distinct from the programming language itself. The [if (condition) ...[ WidgetA, WidgetB ]] syntax (“if collection,” used for example in [lib/app.dart], Chapter 4) takes this logic a step further than [React]: a CONDITION inserted directly within a literal list of widgets, without a ternary operator or intermediate function.
4.7. Dart: a close cousin of TypeScript, not JavaScript
[Dart] was never intended as an alternative to [TypeScript] for the web (unlike the latter, which was specifically designed as a superset of [JavaScript]): it is an independent language, STATICALLY typed from the outset (no implicit “[any]” mode), compiled—never interpreted as-is. For a developer coming from [TypeScript] (like the three previous variants in this course), the essentials translate almost directly: classes, interfaces ([Dart] uses [abstract class] or simple implemented classes, rather than a separate [interface] keyword), generics ([ServerResponse<T>], see Chapter 4), [async]/[await] on [Future<T>] (the EXACT equivalent of [Promise<T>] in [TypeScript]), and a nullability check built into the type system ([String?] rather than a string that is always present—the equivalent of the [strict] mode of [TypeScript], but enforced by the language itself rather than by a compiler option).
A notable difference, encountered in every file in the following chapter that decodes JSON ([core/models/appointment_models.dart]): [TypeScript] removes its types at compile time—a [interface] is checked only at compile time, a decoded JSON object is then used AS IS at runtime ([JSON.parse(...) as ServerResponse<T>], a simple promise made to the compiler). [Dart], on the other hand, has its types determined at runtime: [jsonDecode(...)] always returns dynamic types ([Map<String, dynamic>]), which must be explicitly converted to class instances using a hand-written [fromJson(...)] constructor—more verbose at first, but a missing key or one of an unexpected type then raises an immediate and clear exception, rather than propagating silently.
4.8. A first minimal example
Here is the smallest [Flutter] application that displays something on the screen—compare this with [lib/main.dart]/[lib/app.dart] from Chapter 4, which are significantly more feature-rich but built using the EXACTLY the same building blocks ([runApp(...)], [MaterialApp], and a root widget).
Let's go over this code:
- Line 1: [import 'package:flutter/material.dart';] — equivalent to [import { ... } from 'vue']: [material.dart] includes all the “Material Design” widgets ([Google]) used in this document—buttons, text fields, app bars, etc. [Flutter] also offers a set of [Cupertino] widgets (with the [iOS] style), which are not used in this course, for those who prefer this style on mobile devices ([Apple]).
- Line 3: [void main() {] — the entry point of a [Dart] program, EXACTLY like that of a program [C]/[Java] - [Flutter] has no equivalent to the [HTML] of a web page, into which a [main.ts] script would then be embedded: the application launches HERE directly.
- Line 4: [runApp(...)] — starts the rendering engine [Flutter] and provides it with the root widget to display—the equivalent of [createApp(App).mount('#app')] on the [Vue.js] side ([src/main.ts]), in a single call rather than two.
- lines 5–9: [MaterialApp(home: Scaffold(body: Center(...)))] — a TREE of nested widgets, each with a specific responsibility: [MaterialApp] configures the theme and navigation; [Scaffold] provides the standard screen structure (area for an application bar, content area, etc.); [Center] centers its child; [Text] displays a string. None of these four widgets is a special case of the language: they are simply [Dart] CLASSES, like the ones this course will write itself in the next chapter.
4.9. Summary: Angular -> React -> Vue.js -> Flutter
See also the [README.md] included with the code for this chapter for the full version.
- [Angular] components / [.vue] files / [.tsx] files → [Widgets] [Dart] ([StatelessWidget]/[StatefulWidget])
- [signal()]/[ref()]/[useState()] → mutable fields + [setState(...)] (local state), [ChangeNotifier] (shared state)
- [input()]/[output()], [defineProps]/[defineEmits], function properties → constructor parameters (inputs), functions passed as parameters (outputs)
- injectable/composable services + [provide]/[inject]/[Context] → service classes + package [provider]
- separate template [Angular], [<template>], [Vue.js], [JSX], [React] → method [build(BuildContext)] (pure Dart)
- [HttpClient]/[fetch] → package [http]
- [localStorage] → [shared_preferences]
- No router (single-screen application, in all 5 variants of this course)