Mental Model Quickstart
Four ideas unlock Flutter for React developers. Once these click, the rest of the framework is just learning the widget API — the mental model is already yours.
1. Everything is a widget
Section titled “1. Everything is a widget”In React, your UI has components (your code) and HTML elements (<div>, <p>, <button>). Two distinct layers.
In Flutter there is only one layer: widgets. Layout, spacing, colour, text, images, gesture detection, even the app itself — all widgets. There is no HTML underneath. Flutter draws every pixel directly.
flowchart TB
subgraph React
direction TB
RApp["<App>"] --> RDiv["<div className='card'>"]
RDiv --> RH2["<h2>Hello</h2>"]
RDiv --> RBtn["<button>Click</button>"]
end
subgraph Flutter
direction TB
FApp["MyApp (StatelessWidget)"] --> FMaterial["MaterialApp"]
FMaterial --> FScaffold["Scaffold"]
FScaffold --> FCenter["Center"]
FCenter --> FColumn["Column"]
FColumn --> FText["Text('Hello')"]
FColumn --> FBtn["ElevatedButton(...)"]
end The depth feels greater at first. But it is consistent — there are no two mental models to switch between.
2. The widget tree
Section titled “2. The widget tree”React’s component tree and Flutter’s widget tree are the same concept. Both are descriptions of what should be rendered. Both are re-evaluated when state changes. The framework diffs the description against the previous one and updates only what changed.
// React component treefunction Card({ title, body }) { return ( <div className="card"> <h3>{title}</h3> <p>{body}</p> </div> );}
function App() { return ( <main> <Card title="Flutter" body="Everything is a widget" /> <Card title="React" body="Everything is a component" /> </main> );}// Flutter widget treeclass InfoCard extends StatelessWidget { final String title; final String body; const InfoCard({super.key, required this.title, required this.body});
@override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 8), Text(body), ], ), ), ); }}
class MyApp extends StatelessWidget { const MyApp({super.key});
@override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Column( children: const [ InfoCard(title: 'Flutter', body: 'Everything is a widget'), InfoCard(title: 'React', body: 'Everything is a component'), ], ), ), ); }}3. Hot Reload ≈ Fast Refresh
Section titled “3. Hot Reload ≈ Fast Refresh”Save a file → the running app updates in under a second without losing state. This is Flutter’s Hot Reload, and it works identically to React’s Fast Refresh:
- Edits to
build()methods are injected immediately. - Widget state is preserved (the counter keeps its count).
- Changes to
initState(), class fields, ormain()require Hot Restart (R) to take effect — just like changes to module-level code in React require a manual refresh.
4. Declarative UI — the same idea you know
Section titled “4. Declarative UI — the same idea you know”Flutter is declarative. You describe what the UI should look like for a given state — you do not imperatively mutate the DOM. This is the same contract React introduced.
// Imperative (jQuery-style thinking — don't do this)// "find the Text widget and update its text to '$_count'"
// Declarative (Flutter way)// "given _count, build this subtree"Text('Count: $_count')When setState runs, Flutter calls build() again and the Text widget gets the new value. You describe the outcome, Flutter figures out the minimal update.
Runnable example — all four concepts together
Section titled “Runnable example — all four concepts together”import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
// 1. Everything is a widget (even the app root)
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
useMaterial3: true,
),
// 2. Widget tree: MyApp > MaterialApp > Scaffold > ...
home: const WidgetTreeDemo(),
);
}
}
// 4. Declarative: build() describes the UI for the current state
class WidgetTreeDemo extends StatefulWidget {
const WidgetTreeDemo({super.key});
@override
State<WidgetTreeDemo> createState() => _WidgetTreeDemoState();
}
class _WidgetTreeDemoState extends State<WidgetTreeDemo> {
int _likes = 0;
bool _bookmarked = false;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('Mental Model Demo'),
backgroundColor: colors.primaryContainer,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 3. Try editing this Text and hot-reloading
Text(
'Flutter Mental Model',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'Everything is a widget. The tree is declarative. '
'Hot Reload keeps your state while updating the UI.',
),
const SizedBox(height: 24),
Row(
children: [
FilledButton.icon(
onPressed: () => setState(() => _likes++),
icon: const Icon(Icons.favorite),
label: Text('$_likes Likes'),
),
const SizedBox(width: 12),
OutlinedButton.icon(
onPressed: () => setState(() => _bookmarked = !_bookmarked),
icon: Icon(
_bookmarked ? Icons.bookmark : Icons.bookmark_border,
),
label: Text(_bookmarked ? 'Saved' : 'Save'),
),
],
),
],
),
),
);
}
}