Flutter の内部: より良いコードを書くためのフレームワークを理解する
Flutter は 3 つの並列かつ同期されたツリーに基づいて構築されています。
彼は開発者のことを直接知ったことはないが、彼らがあらゆる側面を決定している
アプリのパフォーマンスと動作。理解する ウィジェットツリー、
の要素ツリー そして レンダーオブジェクトツリー 道を変える
Flutter コードを記述する場所: 理由を説明する const そして勝利のパフォーマンス、
なぜ setState InheritedWidget は再構築せずにデータを伝播するため、ローカルです。
GlobalKey は危険なので役に立ちません。
この記事は理論的なものではありません。各概念には、次のような実践的な例が付いています。 パフォーマンスとアーキテクチャへの実際の影響。最後に考えられるのは、 これは、Flutter がフレームを描画するときに実際に発生します。
何を学ぶか
- 3 つの Flutter ツリー: Widget、Element、RenderObject
- ウィジェットは不変であり、フレームごとに再構築されるため
- Element がライフサイクルと状態を管理する方法
- 調整プロセス: Flutter がフレーム間の差異を見つける方法
- なぜ
constウィジェットは要素ツリーの再構築を回避します - として
setState変更されるサブツリーのみをマークします - InheritedWidget: カスケード再構築を行わないデータ伝播
- レイアウト パス: コンストレイントを下げ、サイズを上げ、ペイント中の位置を変更します。
- RepaintBoundary: 再ペイント ゾーンを分離します。
フラッターの三本の木
Flutter ウィジェットを作成するときは、UI を宣言的に記述することになります。 Flutter はその記述を取得し、内部的に 3 つの異なるデータ構造を構築します。 さまざまな責任を持って。この分離が Flutter のパフォーマンスの基礎です。
| Albero | 可変性 | 責任 | 間隔 |
|---|---|---|---|
| ウィジェットツリー | 不変 | UI構成の説明 | 短い (ビルドごとに再構築) |
| 要素ツリー | 可変 | ライフサイクル、状態、調整 | 長い (再構築間で持続) |
| レンダーオブジェクトツリー | 可変 | レイアウト、ヒットテスト、ペイント | 長い(更新が遅い) |
ウィジェット ツリー: 不変の説明
Flutter のウィジェットと 不変の構成: 方法を説明します
UI の一部が表示されるはずですが、状態やレンダリングは含まれていません。
電話をかけるたびに build(), Flutter は新しい Widget オブジェクトを作成します —
ウィジェットは設定オブジェクトを割り当てるだけなので経済的な運用が可能
ヒープ上に配置され、すぐにガベージ コレクションの対象になります。
# Dimostrazione: Widget = configurazione immutabile
// Questo codice crea nuovi oggetti Widget ogni frame di build:
class CounterWidget extends StatefulWidget {
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
@override
Widget build(BuildContext context) {
// build() restituisce un NUOVO oggetto Text ogni volta che viene chiamato
// Ma Flutter NON ricrea il RenderObject corrispondente ogni volta
// Controlla invece se la configurazione e "uguale" tramite il type + key
return Column(
children: [
// NUOVO oggetto Text creato ogni build, ma efficiente:
Text('Count: $_count'), // runtimeType: Text
ElevatedButton(
onPressed: () => setState(() => _count++),
child: const Text('Increment'), // const: NON ricrea il Widget
),
],
);
}
}
// const Text('Increment') e allocato UNA SOLA VOLTA in memoria
// come costante compile-time. Ogni rebuild del parent usa lo stesso oggetto.
// Questo e il motivo per cui flutter lint raccomanda sempre const.
要素ツリー: ライフサイクル マネージャー
要素ツリーは、Flutter フレームワークの中心です。要素ツリーの各ノード ツリー内のウィジェットと一致しますが、再構築しても保持されます。エレメントは知っている 現在のウィジェット、(StatefulElement 経由で) 状態を管理し、決定します RenderObject を更新するか、新しいものを作成するか。
# Il processo di reconciliation (diffing) di Flutter
// Scenario: aggiorno il type di un widget in una lista
// PRIMA del rebuild:
Column(
children: [
Text('Hello'), // Element: TextElement
Icon(Icons.star), // Element: LeafRenderObjectElement
],
)
// DOPO il rebuild:
Column(
children: [
Text('Hello'), // stesso type -> Flutter RIUSA l'Element
ElevatedButton( // type diverso -> Flutter DISTRUGGE vecchio Element
onPressed: () {}, // e CREA nuovo Element (e RenderObject)
child: const Text('OK'),
),
],
)
// La regola: Flutter fa il match degli Element per POSIZIONE e TIPO (+ Key se presente)
// Se type corrisponde: aggiorna la configurazione dell'Element esistente
// Se type non corrisponde: distrugge il vecchio Element e crea tutto da zero
// IMPLICAZIONE: le liste dinamiche DEVONO usare Key
// SENZA Key (SBAGLIATO):
ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text(items[index].name), // no key!
),
)
// Se riordini la lista, Flutter puo assegnare lo stato sbagliato agli item
// CON Key (CORRETTO):
ListView.builder(
itemBuilder: (context, index) => ListTile(
key: ValueKey(items[index].id), // chiave unica e stabile
title: Text(items[index].name),
),
)
StatefulWidget: 国家が存在する場所
新しい Flutter 開発者から最もよく聞かれる質問の 1 つは、「なぜ状態が
ウィジェットを再構築すると失われませんか?」答えは要素ツリーにあります。
状態は要素内 (具体的には StatefulElement)、そうではありません
ウィジェットで。
# Perche lo stato persiste tra rebuild
// StatefulWidget: Widget (immutabile) + State (mutabile)
class MyWidget extends StatefulWidget {
const MyWidget({super.key, required this.title});
final String title; // configurazione immutabile
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
// LO STATO VIVE QUI, nell'oggetto State
// L'Element tiene un riferimento a questo State
int _counter = 0;
@override
Widget build(BuildContext context) {
// Il Widget (MyWidget) viene ricreato, ma _MyWidgetState persiste
// L'Element aggiorna il suo _widget (la nuova configurazione)
// ma mantiene il suo _state (il vecchio State)
return Text('${widget.title}: $_counter');
// widget.title viene aggiornato automaticamente dall'Element
// quando il parent ricostruisce con un nuovo title
}
}
// Lifecycle completo di StatefulElement:
// 1. Element.mount() -> State.initState()
// 2. Element.update() -> State.didUpdateWidget() (se widget parent ricostruisce)
// 3. Element.deactivate() -> State.deactivate() (rimosso temporaneamente dal tree)
// 4. Element.unmount() -> State.dispose() (rimosso definitivamente)
// IMPORTANTE: dispose() DEVE liberare tutte le risorse
// (AnimationController, StreamSubscription, TextEditingController, etc.)
@override
void dispose() {
_controller.dispose(); // AnimationController
_subscription.cancel(); // StreamSubscription
_textController.dispose(); // TextEditingController
super.dispose();
}
RenderObject ツリー: レイアウトとペイント
RenderObject ツリーはメタルに最も近いレベルであり、レイアウトを管理します。 (位置とサイズの計算)、ヒットテスト (どのウィジェットがタッチに反応するか) そしてペイント(キャンバスに描く)。要素ツリーと比較すると、RenderObject ツリーは 必要な場合にのみ更新され、更新は増分です。
# Il Layout pass: constraints down, sizes up, positions during paint
// Flutter usa un sistema di constraint-based layout:
// 1. Il parent passa CONSTRAINTS al child (max/min width/height)
// 2. Il child calcola la sua SIZE all'interno dei constraints
// 3. Il parent posiziona il child durante il paint
// Esempio: come Column calcola il layout
Column(
children: [
// 1. Column passa: BoxConstraints(maxWidth: 390, maxHeight: infinity)
Text('Hello'),
// Text risponde: "ho bisogno di 40px height"
// 2. Column passa constraints rimanenti al secondo child
Expanded(
// Expanded usa TUTTA l'altezza rimanente
child: ListView(...),
),
],
)
// RepaintBoundary: isola una zona dal ciclo di repaint
// Usalo per widget che si aggiornano spesso e non devono
// far ridisegnare il resto della UI
RepaintBoundary(
child: AnimatedProgressBar(progress: _progress),
)
// Senza RepaintBoundary: ogni frame del progressbar ridisegna tutta la pagina
// Con RepaintBoundary: solo il progressbar viene ridisegnato ogni frame
// CustomPainter con shouldRepaint ottimizzato:
class ChartPainter extends CustomPainter {
const ChartPainter({required this.data, required this.color});
final List<double> data;
final Color color;
@override
void paint(Canvas canvas, Size size) {
// ... codice di disegno
}
@override
bool shouldRepaint(ChartPainter oldDelegate) {
// Ridisegna SOLO se i dati o il colore sono cambiati
// Confronto reference per le liste (usa listEquals per confronto profondo)
return oldDelegate.data != data || oldDelegate.color != color;
}
}
const ウィジェット: 誤解されているパフォーマンスの勝利
キーワード const ウィジェットの設定は単なるスタイルの好みではありません。
そして、要素ツリーでの調整作業を排除する根本的な最適化です。
# const: perche e una performance win concreta
// SENZA const: ogni build() del parent crea nuovi oggetti
class ParentWidget extends StatefulWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// NUOVO oggetto Text creato ogni rebuild del parent
// Flutter deve fare il confronto nell'Element tree
Text('Static label'),
// widget che cambia davvero
Text('Dynamic: $_counter'),
],
);
}
}
// CON const: Flutter usa lo stesso oggetto compile-time
class ParentWidget extends StatefulWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// Stesso oggetto compile-time, NESSUN confronto nell'Element tree
const Text('Static label'),
// Solo questo widget causa lavoro all'Element tree
Text('Dynamic: $_counter'),
],
);
}
}
// La regola: const rende il Widget una "costante compile-time"
// L'Element tree vede che il Widget e identico (stesso riferimento in memoria)
// e salta completamente la fase di reconciliation per quel sottoalbero
// flutter lint: "prefer_const_constructors" ti dice dove aggiungere const
// flutter analyze conta quante opportunita stai perdendo
InheritedWidget: 効率的なデータ伝達
InheritedWidget は、Flutter がデータをツリーの下に伝播するメカニズムです。 各レイヤーにパラメーターを渡す必要はありません。そして、テーマの基礎である MediaQuery は、 Navigator と Riverpod 自体。
# InheritedWidget: come funziona internamente
// Implementazione manuale (come funziona Theme, MediaQuery, etc.)
class AppConfig extends InheritedWidget {
const AppConfig({
super.key,
required this.apiBaseUrl,
required this.isDarkMode,
required super.child,
});
final String apiBaseUrl;
final bool isDarkMode;
// Metodo statico per accedere dal tree discendente
static AppConfig of(BuildContext context) {
final config = context.dependOnInheritedWidgetOfExactType<AppConfig>();
assert(config != null, 'AppConfig non trovato nel tree');
return config!;
}
// CRITICO: updateShouldNotify decide quali widget discendenti si ricostruiscono
// Restituisci TRUE solo se il dato e davvero cambiato
@override
bool updateShouldNotify(AppConfig oldWidget) {
return apiBaseUrl != oldWidget.apiBaseUrl ||
isDarkMode != oldWidget.isDarkMode;
}
}
// Utilizzo: qualsiasi widget discendente puo accedere ad AppConfig
class SomeDeepWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// context.dependOnInheritedWidgetOfExactType registra questo widget
// come "dipendente" da AppConfig: si ricostruira quando AppConfig cambia
final config = AppConfig.of(context);
return Text(config.isDarkMode ? 'Dark Mode' : 'Light Mode');
}
}
// DIFFERENZA IMPORTANTE:
// context.dependOnInheritedWidgetOfExactType -> si iscrive agli update (rebuild)
// context.getInheritedWidgetOfExactType -> legge senza iscriversi (no rebuild)
// Questa distinzione e il motivo per cui:
// Theme.of(context) -> causa rebuild quando il tema cambia
// Theme.of(context).colors -> stesso effetto, non filtra per sub-proprieta
setState: ローカル スコープとそれが効率的な理由
# setState: marca solo il sottoalbero necessario
// setState non "ricostruisce tutta l'app" - marca solo l'Element di QUESTO State
// come "dirty" nella lista dei widget da rebuildarsi nel prossimo frame
class CounterPage extends StatefulWidget {
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
// AppBar non dipende da _count -> NON si ricostruisce
appBar: const AppBar(title: Text('Counter')),
body: Center(
// SOLO questo Text si ricostruisce (il suo Element viene marcato dirty)
child: Text('$_count'),
),
floatingActionButton: FloatingActionButton(
// La FAB non dipende da _count -> NON si ricostruisce
onPressed: () => setState(() => _count++),
child: const Icon(Icons.add),
),
);
}
}
// Per ridurre ulteriormente il scope del rebuild: estrai il widget che cambia
// in un StatefulWidget separato con il suo setState locale
// ANTI-PATTERN: setState al livello piu alto per dati condivisi
// SOLUZIONE: Riverpod, InheritedWidget o altri state management
// che permettono rebuild granulari solo ai widget che usano quel dato
キー: タイプとその使用時期に関するガイド
# Keys: ValueKey, ObjectKey, UniqueKey, GlobalKey
// ValueKey: key basata su un valore primitivo (id, stringa)
// USO: liste ordinate/filtrate dove gli item possono spostarsi
ListView.builder(
itemBuilder: (context, index) => ProductCard(
key: ValueKey(products[index].id), // usa l'id del dominio, stabile
product: products[index],
),
)
// ObjectKey: key basata su un oggetto (confronto per identita)
// USO: quando hai un oggetto come chiave unica
ValueKey(product.id) // preferibile: confronta l'ID (stringa/int)
ObjectKey(product) // confronta il riferimento all'oggetto
// UniqueKey: genera una key unica ogni volta (non stabile tra rebuild!)
// USO: forzare la ricostruzione di un widget (reset del suo stato)
UniqueKey() // ATTENZIONE: ogni rebuild crea una key diversa = State perso
// GlobalKey: accede allo State o al RenderObject da fuori del tree
// RARO: evita quando possibile, ha overhead significativo
final _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: Column(children: [...]),
)
// Poi: _formKey.currentState!.validate()
// REGOLA: usa GlobalKey solo per Form, ScaffoldMessenger, Navigator
// Per tutto il resto: passa callbacks o usa state management
パフォーマンス チェックリスト: Flutter 内部の実践
-
可能な限り定数: すべてのウィジェットは静的でなければなりません
const。 lint ルールを有効にするprefer_const_constructors. -
動的リストにキーを入力します。 どれでも
ListView.builderoColumn動的子では使用する必要がありますValueKeyドメイン ID に基づいて。 - 最下位レベルの setState : 別の StatefulWidget に抽出する ルートレベルで setState を呼び出す代わりに変更される UI の部分。
-
アニメーションの RepaintBoundary: 連続アニメーションをラップする
(スピナーの読み込み、進行状況バー、カウントダウン)
RepaintBoundary. -
CustomPainter での shouldRepaint : 常にロジックを実装する
常に返すのではなく正しい
true. - 不要な GlobalKey は不要です。 各 GlobalKey にはオーバーヘッドがあります。使ってください 厳密に必要な場合のみ (フォーム、スキャフォールド、ナビゲーター)。
結論
Flutter の 3 つのツリー (Widget、Element、RenderObject) の変換を理解する
「曖昧なベストプラクティス」から因果関係の理解に至るまで、あらゆるコードの決定:
まさになぜ const それは機能します、なぜなら setState e
InheritedWidget によって不必要なカスケード再構築が発生しないため、効率的です。
この内部知識は学術的な詳細ではなく、それが人間性を分けるものです。 60fps でアプリを構築する人から「物事を機能させる」Flutter 開発者 保証され、予測可能なメモリ フットプリントとパフォーマンスを維持するアーキテクチャ コードベースが成長するにつれて。フレームワークは正確なアーキテクチャ上の選択を行っています — それらを理解するということは、フレームワークに対抗するのではなく、フレームワークと協力できることを意味します。







