อภิธานศัพท์
ศัพท์ของ Flutter ครอบคลุมหลายชั้นพร้อมกัน: ตัวภาษา Dart เอง (null safety, record, pattern, class modifier) ที่ทุกชั้นอื่นเขียนอยู่บนมัน; สามต้นไม้ (widget, element, render) ที่อธิบายเรื่อง performance ได้เกือบทุกอย่าง; state management (setState, InheritedWidget, Riverpod, BLoC) ที่สร้างอยู่บน element tree นั้นโดยตรง; การนำทาง (Navigator, go_router) และ platform channel สำหรับออกไปนอก Dart; อัลกอริทึม layout แบบ constraints กับ pipeline paint/composite/raster ที่อยู่ใต้ทุกพิกเซล; และเรื่อง tooling/production (testing, build, crash reporting) ที่เปลี่ยน widget tree ให้กลายเป็นแอปที่ส่งออกจริง เวลาอ่านบทเรียนแล้วเจอคำที่ไม่คุ้น หรือไม่แน่ใจว่ายังใช้ได้อยู่กับ Flutter 3.47.5 / Dart 3.13 ไหม ให้กลับมาดูที่นี่ — ทุกคำนิยามด้านล่างมาจากบทเรียนที่พิสูจน์ด้วยการรัน flutter analyze/flutter test จริง หรือ quote source/doc ของ SDK จริง ไม่ใช่จากความจำ
| Term | ความหมาย | บทเรียน |
|---|---|---|
| AnimatedOpacity / FadeTransition | widget animation แบบ implicit ที่ห่อกลไก RenderOpacity/layer เดียวกับที่พิสูจน์แล้วสำหรับ widget Opacity ธรรมดา — มันเพิ่มแค่การจัดตารางด้วย ticker กับการ dispose อัตโนมัติ ไม่ได้ ทำให้ไม่ต้องจ่ายค่า compositing layer จริงในทุกเฟรมที่ alpha ไม่เท่ากับศูนย์ระหว่าง fade | Animations |
| AnimatedSwitcher | สลับ (cross-fade) ระหว่าง widget สองตัวเมื่อ child ของมันเปลี่ยน — จะ animate หรือไม่ตัดสินจาก key ของ child: key เดิมแปลว่า “widget เดิม อัปเดตในที่” ไม่มี transition; key ต่างแปลว่า “widget ใหม่” แล้วมันจะ animate การสลับ | Animations |
| AnimationController | ตัวขับ animation แบบ explicit: vsync: this (ต้องใช้ TickerProviderStateMixin), .value ไล่จาก 0→1 ตลอด duration — พิสูจน์ตรง ๆ ด้วย tester.pump(Duration) ที่ได้ค่ากลางที่แม่นยำ (0.50 ที่ครึ่งทางเมื่อใช้ Curves.linear) ลืม controller.dispose() จะโยน assert จริงที่ทำงานเฉพาะ debug mode: "was disposed with an active Ticker" จาก SingleTickerProviderStateMixin.dispose() ใน SDK Flutter 3.47.5 ที่ติดตั้งจริง | Animations |
analysis_options.yaml (strict-casts / strict-inference / strict-raw-types) | ตัวเลือก language ของ analyzer สามตัว — ปิดโดย default ไม่ใช่ส่วนหนึ่งของ lint package ไหน — ที่จับ dynamic แฝงที่หลุดเข้ามาในโค้ดที่ควรมี type ชัดเจน แต่ละตัวมี diagnostic จริงต่างกัน: strict-casts ให้ invalid_assignment ตอน cast แบบ implicit จาก dynamic → int; strict-raw-types ให้ strict_raw_type ตอนเจอ List เปล่า ๆ; strict-inference ให้ inference_failure_on_collection_literal ตอน [] ที่ infer ไม่ได้ probe ของคอร์สนี้เปิดทั้งสามตัวจริง | Project & Tooling |
| async / await | async ทำให้ function คืนค่าเป็น Future<T>; await แกะมันออกมาแล้วปล่อยให้ error ปรากฏเป็น exception ปกติที่ try/catch ได้ — พิสูจน์ตรง ๆ กับ Future ที่ reject แล้วจริง แทบไม่ await ใน build เลย แต่ส่ง Future ให้ FutureBuilder แทน | Async in Dart |
AsyncNotifier / AsyncValue.when | provider สำหรับ async state ของ Riverpod: build() คืน Future<T> แล้ว ref.watch จะได้ AsyncValue<T> มาแยกกิ่งด้วย .when(data:, error:, loading:) Riverpod 3 ให้ทุก provider มี retry policy อัตโนมัติ (ProviderContainer.defaultRetry, exponential backoff, จำกัดจำนวนครั้ง) — error ที่ถูก throw จะยังเป็น AsyncLoading(error: ..., retrying: true) ไม่ใช่ AsyncError ตราบเท่าที่ยังมีการ retry ค้างอยู่ ซึ่งเป็นกับดักจริงที่พิสูจน์แล้วสำหรับ widget test ที่คาดหวัง error state ทันที | Provider & Riverpod |
.autoDispose | ตัวปรับแต่ง provider ของ Riverpod ที่ dispose state ของ provider ทันทีที่ listener ตัวสุดท้าย unsubscribe แทนที่จะ cache ไว้ตลอดอายุแอป — เป็นค่า default ที่ควรใช้กับ family/provider ที่รับ parameter เพื่อไม่ให้เกิด instance ค้างต่อ parameter หนึ่งค่าตลอดไป | Provider & Riverpod |
AutomaticKeepAliveClientMixin | ทำให้ State ของ list item ตัวใดตัวหนึ่งไม่ถูก “dispose ทันทีที่เลื่อนออกนอกจอ” ตามปกติ — พิสูจน์ด้วยการทดสอบจริง: เลื่อนออกไป 8000px แล้วเลื่อนกลับ item ที่ keep-alive จะได้ State ตัวเดียวกัน (id เดิม) ในขณะที่ State ธรรมดาจะเป็นตัวใหม่จริง ๆ แลกมาด้วยการเสียประโยชน์ด้าน memory ที่ laziness ปกติให้ไว้ ใช้เฉพาะกับ state ที่ rebuild แพงจริง ๆ | Slivers & Scrolling |
| Term | ความหมาย | บทเรียน |
|---|---|---|
BackgroundIsolateBinaryMessenger | ทำให้เรียก platform channel จาก background isolate ได้ (ข้างใน compute()/Isolate.run) แทนที่จะได้แค่ root isolate — ต้อง ensureInitialized(token) ก่อน โดย token เป็น RootIsolateToken ที่ได้จาก root isolate ผ่าน ServicesBinding.rootIsolateToken ก่อน spawn ถ้าข้ามขั้นตอนนี้จะโยน error จริง: instance “invalid จนกว่าจะรัน ensureInitialized” ตาม doc comment ของ SDK 3.47.5 ที่ติดตั้งจริง | Platform Channels |
base (class modifier) | ห้าม implements นอก library ที่ประกาศ แต่ยังอนุญาต extends — error จริง invalid_use_of_type_outside_library: “can’t be implemented outside of its library because it’s a base class” รับประกันว่าทุก instance เป็นชนิดจริงผ่านการสืบทอดจริง ไม่ใช่ของปลอมที่แค่สร้างหน้าตาให้ตรง interface | Classes & Mixins |
BLoC / Bloc | pattern ที่บังคับให้ data flow ไปทิศทางเดียวอย่างเคร่งครัด: UI ส่ง event เข้าไป Bloc<Event, State> emit state ออกมา UI rebuild จาก state นั้น — ไม่เคย mutate state ตรง ๆ flutter_bloc (9.1.1) มีทั้ง Bloc (มี class Event ชัดเจน) และ Cubit ที่ง่ายกว่า | BLoC & Patterns |
BlocBuilder / buildWhen | BlocBuilder<B, S> rebuild ทุกครั้งที่มี state ใหม่โดย default; buildWhen: (previous, current) => ... ใช้จำกัดขอบเขตนั้น — พิสูจน์ด้วยตัวนับ build: สี่ event Increment ที่ runtimeType ของ state ไม่เปลี่ยนทำให้ build เป็นศูนย์ครั้ง แล้ว event ที่ห้า (ที่ type เปลี่ยนจริง) ทำให้ build หนึ่งครั้ง บทบาทเดียวกับ aspect ของ InheritedModel หรือ .select() ของ Riverpod | BLoC & Patterns |
BlocListener | รัน side effect ครั้งเดียว (การนำทาง, SnackBar) ตอบสนองต่อ state ใหม่ โดยไม่ rebuild widget ไหนเลย — พิสูจน์แล้วว่ายิงพอดีครั้งเดียว ด้วยค่า state ที่ trigger จริง ตลอดห้า event | BLoC & Patterns |
blocTest | helper ของ bloc_test (10.0.0): build bloc ขึ้นมา ขับด้วย act แล้ว assert ลำดับ state ที่ emit ออกมาแบบเป๊ะ ด้วย expect: () => [...] — ไม่มี widget เกี่ยวข้องเลย seed: ใช้เริ่มจาก state ที่กำหนดโดยไม่ต้อง replay event ที่ปกติต้องทำถึงจะไปถึง state นั้น ต้องให้ class ของ state extend Equatable (ดูด้านล่าง) ไม่งั้น state ที่ค่าเท่ากันจะเทียบไม่เท่ากันแล้ว assertion ทุกอันจะ fail | BLoC & Patterns |
BuildContext | ไม่ใช่ตัวแปร global ลอย ๆ — มัน คือ Element ณ ตำแหน่งนั้นในต้นไม้ (พิสูจน์ด้วยการ spy createElement) identity นี้เองที่ทำให้ .of(context) ค้นหาขึ้นไปข้างบนหา ancestor ได้ การใช้ context ที่อยู่เหนือ ancestor ที่กำลังหา (เช่น Scaffold.of(context) ใน build เดียวกับที่สร้าง Scaffold) จะโยน error จริงชื่อชัดเจน — Builder คือทางแก้มาตรฐาน เพราะสร้าง context ที่อยู่ใต้จริง ๆ | Build & BuildContext |
build / rebuild / dirty | setState (หรือ ancestor ที่ rebuild) จะทำเครื่องหมาย element ของ widget นี้เป็น dirty; เฟรมถัดไป Flutter จะเรียก build ใหม่กับทุก element ที่ dirty build ต้อง pure และเร็ว — ห้ามมี side effect ห้ามสร้าง controller ห้ามเรียก network เพราะมันอาจถูกเรียกทุกเฟรมของ animation ไม่ใช่แค่ครั้งเดียว | setState & Lifecycle |
| Term | ความหมาย | บทเรียน |
|---|---|---|
cacheWidth / cacheHeight | ตัวบอกขนาดตอน decode ของ Image เป็นพิกเซลจริง (physical pixels): การ decode ภาพต้นฉบับขนาดใหญ่เต็ม resolution ทั้งที่จะแสดงแค่ขนาด thumbnail เปลืองทั้ง memory และเวลา decode การส่งขนาดที่จะแสดงจริงเข้าไปตรงนี้จะจำกัดสิ่งที่ image decoder จองไว้ | Rebuilds & Performance |
catchError เทียบกับ .then(onValue, onError:) | ต่างกันจริง พิสูจน์ตรง ๆ: onError บน .then จับ error จาก future ที่ chain ต่อเข้ามาเท่านั้น ไม่จับ error ที่ throw ข้างใน onValue เอง; .catchError(...) ที่ chain ต่อจาก .then() จับ error จากทั้งสองทาง สลับใช้ผิดกันคือบั๊กจริงที่กลืน error แบบเงียบ ๆ | Async in Dart |
ChangeNotifierProvider | การผูก InheritedWidget + ChangeNotifier ของ Provider: notifyListeners() จะ trigger การ rebuild ของ InheritedWidget ข้างใต้; context.watch<T>() คือ dependOnInheritedWidgetOfExactType ที่เขียนสวยขึ้น context.read<T>() คือการอ่านแบบไม่ผูก dependency | InheritedWidget |
class modifiers (sealed / final / base / interface / mixin class) | modifier 5 ตัวของ Dart 3.0 ที่จำกัดว่า class จะถูกใช้จากนอก library ที่ประกาศได้อย่างไร — ดูรายละเอียดแต่ละตัวในหัวข้อของมันเอง sealed/base/interface/final ทั้งสี่ตัวใช้ analyzer error code เดียวกัน คือ invalid_use_of_type_outside_library — ต่างกันแค่ข้อความ; grep CI output ให้ดูที่ code ไม่ใช่ข้อความ | Classes & Mixins |
Clip / Clip.antiAliasWithSaveLayer | enum พฤติกรรม clip ที่ widget clip ทุกตัวใช้: Clip.none (ฟรี เป็น default ของ widget ส่วนใหญ่), Clip.hardEdge (เร็ว แต่ขอบหยัก), Clip.antiAlias (default ของ ClipRRect เอง — เนียน แต่มี artifact ขอบเลือดได้บ้าง) และ Clip.antiAliasWithSaveLayer — โหมดเดียวที่บังคับให้เกิด offscreen buffer ผ่าน Canvas.saveLayer จริง ซึ่ง source ของ dart:ui เองบอกว่า “very slow” ใช้เฉพาะตอนที่ artifact ของ Clip.antiAlias เห็นชัดและรับไม่ได้เท่านั้น | Render Pipeline |
ColorScheme.fromSeed | สร้างพาเลตสีทั้งหมดของแอป Material 3 จากสีเมล็ด (seed) Color เดียว — พิสูจน์ตรง ๆ: Theme.of(context).colorScheme.primary ใน widget ที่กำลังรันจริงตรงกับ ColorScheme.fromSeed(seedColor: ...) ที่ theme ถูกสร้างขึ้นมาเป๊ะทุกไบต์ การอ่านสีจาก Theme.of(context).colorScheme แทนที่จะ hardcode Color เองคือสิ่งที่ทำให้ dark mode และการ rebrand เป็นแค่การเปลี่ยน seedColor บรรทัดเดียว | App Scaffolding |
code generation (@riverpod, build_runner, .g.dart) | riverpod_generator/riverpod_annotation (4.0.7) เขียน boilerplate ของ provider ให้ (.autoDispose, type ที่ถูกต้อง, การต่อ ref) จาก function หรือ class Notifier/AsyncNotifier ที่แปะ annotation ผ่าน part 'x.g.dart' — รันด้วย dart run build_runner build harness ของคอร์สนี้ไม่รัน build_runner ดังนั้นบทเรียนที่ใช้ codegen จะแนบเนื้อหา .g.dart ที่ generate จริงเป็นไฟล์ของตัวเอง | Provider & Riverpod |
compute() | ตัวห่อของ Flutter สำหรับรัน pure function บน background isolate อ่าน source ของ SDK Flutter 3.47.5 ที่ติดตั้งจริงแล้วพบว่าคำกล่าวอ้างทั่วไปที่ว่ามันเพิ่ม “isolate-pool management” เหนือ Isolate.run นั้นไม่จริง — บน native compute(fn, arg) คือ Isolate.run(() => fn(arg)) เป๊ะ ๆ บวกแค่ debug label บน web มันเป็นคนละ function เลย: await null; แล้ว callback(message) บน thread เดียวกัน — ไม่มี isolate เลย ยืนยันจาก api.flutter.dev: “On web platforms this will run callback on the current eventloop.” closure ที่ส่งให้ compute() แล้ว capture native handle (เช่น ReceivePort, BuildContext) จะโยน ArgumentError: Illegal argument in isolate message: object is unsendable จริง | Isolates & compute() |
const canonicalization | expression const ของ widget/record/ค่าใด ๆ คือ object ที่ canonicalize เป็นตัวเดียว — identical(const (1,2), const (1,2)) เป็น true พิสูจน์ตรง ๆ ทั้งกับ record และ widget เส้นทางอัปเดต element ของ Flutter จะ short-circuit ด้วย identical(oldWidget, newWidget) ทำให้ child ที่เป็น const ไม่ถูก diff หรือ rebuild เลย ในขณะที่ sibling ที่ไม่ const จะ rebuild ทุกครั้งที่ parent rebuild — นี่แข็งแรงกว่าการเช็ค memoization มือด้วยซ้ำ และฟรี | Widgets and the Tree |
ConnectionState | enum สี่ค่าที่ AsyncSnapshot ถืออยู่: none (เฉพาะตอน future/stream เป็น null เปล่า ๆ), waiting (subscribe แล้ว ยังไม่มีผล), active (เฉพาะ StreamBuilder — มี event มาแล้วอย่างน้อยหนึ่งอัน stream ยังเปิดอยู่), done (future เสร็จแล้ว หรือ stream ปิดแล้ว) FutureBuilder ให้แค่ waiting/done ในทางปฏิบัติ — ไม่มี state “active” สำหรับค่าครั้งเดียว | Futures & Streams in the UI |
ConstrainedBox / UnconstrainedBox / OverflowBox | ConstrainedBox เพิ่มขอบ min/max พิเศษทับบน constraint ที่รับเข้ามา; UnconstrainedBox ให้ child เพิกเฉย constraint ที่รับเข้ามาแล้วขยายตามขนาดธรรมชาติของตัวเอง (overflow ได้เห็น ๆ โดยตั้งใจ); OverflowBox ทำลายกฎ “ไหลลงมา” จริง ๆ โดยให้ constraint ช่วงที่ต่างออกไปกับ child เทียบกับที่ตัวเองได้รับมา | Constraints & Layout |
| constraints (tight / loose / unbounded) | Tight: min เท่ากับ max child ต้องมีขนาดนั้นเป๊ะ (SizedBox) Loose: min เป็น 0 child เป็นขนาดเท่าไหร่ก็ได้จนถึง max (Center) Unbounded: max เป็นอนันต์ “ใหญ่แค่ไหนก็ได้” — Column ธรรมดาให้ constraint แนวหลักแบบ unbounded กับ child ที่ไม่ใช่ flex ซึ่งนี่แหละคือสิ่งที่ทำให้ ListView เปล่า ๆ ที่วางไว้ตรงนั้นพัง (ดูด้านล่าง) | Constraints & Layout |
context.go เทียบกับ context.push เทียบกับ Navigator.push | context.go(path) แทนที่ location ปัจจุบัน (stack ตามที่ route table ของ go_router บอกไว้สำหรับ location นั้น — ไม่มี back-stack ระหว่าง sibling); context.push(path) เพิ่ม entry ซ้อนขึ้นไปข้างบน ถูก go_router track เป็น ImperativeRouteMatch; Navigator.of(context).push(...) แบบดิบทำให้ stack ของ Flutter เดียวกันโตขึ้นเหมือนกัน แต่ความรู้ของ go_router เรื่อง “location ปัจจุบัน” จะไม่เห็นมันเลย — พิสูจน์ตรง ๆ: หลัง push แบบ imperative currentConfiguration.uri ของ GoRouter ยังเป็น path เดิม | Declarative Routing |
covariant | ทำให้ parameter ของ override ใน subclass รับ type ที่เจาะจงกว่าที่ signature ของ supertype ประกาศไว้ได้ แลกการรับประกันตอน compile-time กับ TypeError ตอน runtime ถ้าเรียกด้วย subtype ผิด — ใช้กับ didUpdateWidget(covariant MyWidget oldWidget) ตลอดทั้งคอร์ส | Classes & Mixins |
| Cubit | ครึ่งที่ง่ายกว่าของ flutter_bloc: ไม่มี class Event มีแค่ method ที่เรียก emit(newState) ตรง ๆ เริ่มจากตัวนี้ก่อน แล้วค่อยไปหา Bloc เต็มรูป (มี event ชัดเจน) เฉพาะตอนที่รายการ input ที่ถูก log และมีเอกสารครบคุ้มค่ากับความยุ่งยากที่เพิ่มขึ้น | BLoC & Patterns |
Curve / Curves | ปรับเวลาเชิงเส้นของ animation ให้เคลื่อนไหวเป็นธรรมชาติ (Curves.easeInOut, Curves.elasticOut, …); ทั้ง TweenAnimationBuilder และ CurvedAnimation ที่ขับด้วย AnimationController รับ curve ได้ ยังทดสอบได้เต็มที่ผ่าน tester.pump(Duration) — assert กับ Curves.easeInOut.transform(0.5) แทนค่าเชิงเส้นดิบ | Animations |
CustomScrollView | ประกอบ sliver หลายแบบเข้าด้วยกัน (SliverAppBar, SliverToBoxAdapter, SliverGrid, SliverList) ให้ scroll ต่อเนื่องเป็นชิ้นเดียว — พิสูจน์ตรง ๆ ว่าแต่ละ sliver ยัง lazy อย่างอิสระแม้อยู่ใน view ที่ประกอบแล้ว (build ไป 13 จาก 1000 แถวของ SliverList ในการรันครั้งหนึ่งที่วัดจริง) | Slivers & Scrolling |
| Term | ความหมาย | บทเรียน |
|---|---|---|
dart fix --dry-run / dart fix --apply | ใช้ auto-fix ที่ analyzer ลงทะเบียนไว้เอง --dry-run แสดงว่าจะเปลี่ยนอะไรโดยไม่แตะอะไรจริง — พิสูจน์ด้วย output จริงของ use_full_hex_values_for_flutter_colors ไม่ใช่ทุก lint จะ auto-fix ได้: avoid_unnecessary_containers เป็น diagnostic จริงที่ dart fix --dry-run รายงาน fix ที่เสนอเป็นศูนย์ เพราะการเอา Container ออกพร้อม re-parent child ให้ถูกต้องเป็นการแก้เชิงโครงสร้างที่อยู่นอกเหนือ fix registry | Project & Tooling |
dart:ffi | กลไกระดับล่างสำหรับเรียก library C โดยตรง (ไม่ผ่าน glue code ของ Kotlin/Swift) — quote จาก doc C-interop ของ dart.dev ตรง ๆ: “Dart… apps… can use dart:ffi… to call native C APIs, and to read, write, allocate, and deallocate native memory.” เป็นเครื่องมือคนละตัวกับ platform channel: channel คุยกับ platform SDK ผ่าน native glue ที่เราเขียน ส่วน dart:ffi คุยตรงกับ C ABI เลย | Platform Channels |
dart:io เทียบกับ web (kIsWeb, conditional import) | ข้อสันนิษฐานทั่วไปที่ผิด: flutter build web จะพังทันทีถ้ามี import 'dart:io' พิสูจน์แล้วว่าไม่จริง — class ของ dart:io มีอยู่จริงเป็น stub ที่ type-check ได้บน web compiler (ยืนยันจาก io_patch.dart ของ SDK ที่ติดตั้งจริง) ดังนั้น build ที่มี Platform.isIOS/File.readAsString() ต่อสายเข้า main() สำเร็จ แม้แต่กับ --wasm บั๊กนี้เลยรอดทะลุ flutter analyze และ build target ทั้งสองแบบแบบเงียบ ๆ แล้วโผล่มาเป็น UnsupportedError จริงตอนที่ browser ของผู้ใช้รันโค้ดเส้นทางนั้นเป็นครั้งแรก kIsWeb (bool ที่รู้ตอน compile-time) หรือ conditional import (import 'io_impl.dart' if (dart.library.js_interop) 'web_impl.dart';) คือตัวป้องกันจริง | Project & Tooling |
--dart-define / --dart-define-from-file | ส่งค่าคงที่ตอน compile-time ที่โค้ดอ่านด้วย String.fromEnvironment('KEY', defaultValue: ...) — พิสูจน์ตรง ๆ: รัน flutter test --dart-define=API_URL=... แล้วค่าคงที่อ่านได้ตรงตามนั้นตอน compile ไม่ใช่การอ่าน environment variable ตอน runtime --dart-define-from-file=config.json ส่งไฟล์ key/value ทั้งไฟล์ทีเดียว เหมาะกับ flavor ที่ต้องการหลายค่า | Build & Release |
| definite assignment analysis | การพิสูจน์ของ analyzer ว่า local variable ที่ non-nullable ถูก assign ครบทุกเส้นทางก่อนถูกอ่าน local ที่ไม่ใช่ final ที่ขาดเส้นทางจะได้ not_assigned_potentially_non_nullable_local_variable; local ที่เป็น final ที่ขาดเหมือนกันจะได้ diagnostic จริงอีกแบบ — read_potentially_unassigned_final — ทั้งสองพิสูจน์ด้วย output จริงของ flutter analyze | Null Safety |
didChangeDependencies | รันทันทีหลัง initState และรันอีกทุกครั้งที่ inherited dependency ที่ State นี้อ่านเปลี่ยนแปลงจริง — ที่ปลอดภัยสำหรับอ่านข้อมูล InheritedWidget/Provider/Theme.of ที่ setup ต้องพึ่งพา (initState เองอ่าน inherited widget ได้ไม่น่าเชื่อถือ) | setState & Lifecycle |
didUpdateWidget | ถูกเรียกเมื่อ parent rebuild แล้วส่ง widget config ใหม่ที่ type/key เดิมมาให้ State นี้ — พิสูจน์แล้วว่ารันก่อน build ที่มันไป trigger ผ่านลำดับ log จริง covariant ขยาย type ของ parameter จาก signature ของ supertype ให้เป็น widget concrete type | setState & Lifecycle |
dispose | ถูกเรียกเมื่อ Flutter ยืนยันแล้วว่า element ไม่ถูกใส่กลับเข้าที่อื่นหลัง deactivate — จุดสำหรับ cleanup controller, ยกเลิก subscription ลืมเรียกคือ memory leak คลาสสิกของ Flutter พิสูจน์เป็นรูปธรรมกับ Ticker ของ AnimationController ที่ leak | setState & Lifecycle |
dependOnInheritedWidgetOfExactType เทียบกับ getInheritedWidgetOfExactType เทียบกับ findAncestorWidgetOfExactType | มีแค่ dependOnInheritedWidgetOfExactType เท่านั้นที่ผูก dependencyแล้ว auto-rebuild ผู้เรียกเมื่อมีการอัปเดต — พิสูจน์ด้วยการนับ build: หลัง InheritedWidget ต้นทางอัปเดต ตัวนับ build ของ dependOn ขยับจาก 1→2 ขณะที่ get/findAncestor ค้างที่ 1 getInheritedWidgetOfExactType คือ lookup แบบ cache O(1) เดียวกันแต่ไม่มี subscription (อ่านครั้งเดียว); findAncestorWidgetOfExactType เดินต้นไม้ widget ตรง ๆ (O(depth) ใช้ได้กับ Widget ใดก็ได้ ไม่จำกัดแค่ InheritedWidget) | InheritedWidget |
| DevTools | ชุดเครื่องมือบน browser สำหรับตรวจแอปที่กำลังรันอยู่ — widget inspector, performance/timeline view, memory view, network view สภาพแวดล้อมของคอร์สนี้ไม่มี device ให้ต่อ ทุกคำกล่าวอ้างเกี่ยวกับ DevTools ในคอร์สนี้จึงอ้างอิงจาก docs ไม่ใช่ screenshot จริง | Project & Tooling |
| Term | ความหมาย | บทเรียน |
|---|---|---|
| Element / element tree | instance ที่มีชีวิต ของ widget ณ ตำแหน่งหนึ่งในต้นไม้ — อยู่ต่อเนื่องข้าม rebuild ถือ State ตัดสินใจว่า widget ใหม่จะอัปเดตมันในที่ (update/didUpdateWidget) หรือต้องรื้อทิ้ง (unmount + createElement ใหม่) BuildContext คือ element (พิสูจน์ด้วยการ spy identity ของมัน) อยู่ตรงกลางของสามต้นไม้ ระหว่าง widget tree ที่ถูกและ render tree ที่แพง | Widgets and the Tree |
| Equatable | package:equatable (3.0.0) ให้ class มี value-equality (==/hashCode) จากรายการ props ที่ประกาศไว้ แทนที่ identity equality ปกติของ Dart — จำเป็นสำหรับ expect: () => [...] ของ blocTest ที่จะเทียบ state ที่ emit ออกมาแบบ by value; ถ้าไม่มี state สองตัวที่หน้าตาเท่ากันแต่สร้างแยกกันจะเทียบไม่เท่ากัน แล้ว assertion ทุกอันจะ fail | BLoC & Patterns |
EventChannel | ต่างจาก MethodChannel ที่เป็นเรียกครั้งเดียว/ตอบครั้งเดียว EventChannel ส่งstreamของ event จากฝั่ง native (ข้อมูล sensor, การเชื่อมต่อ) — map เป็น Stream ของ Dart ผ่าน receiveBroadcastStream() setMockStreamHandler(channel, MockStreamHandler.inline(onListen: ...)) ของ flutter_test ใช้ mock มันได้ พิสูจน์แล้วว่าส่ง event ตามลำดับแล้วปิด | Platform Channels |
| exhaustiveness | การพิสูจน์ของ analyzer ว่า switch เหนือ class ที่เป็น sealed หรือ enum ครอบคลุมทุกกรณีที่เป็นไปได้ ตั้งแต่ compile-time — พิสูจน์ตรง ๆ ด้วยการลบ case หนึ่งออกแล้วได้ diagnostic จริง non_exhaustive_switch_expression/non_exhaustive_switch_statement นี่คือสิ่งที่ทำให้ sealed + switch มั่นคง: analyzer นับทุก subtype ได้เพราะไม่มี library อื่นเพิ่มได้ | Dart Language Tour |
Expanded เทียบกับ Flexible | ทั้งคู่แบ่งพื้นที่ที่เหลือของ Row/Column ด้วย flex factor แต่ให้ constraint กับ child ต่างกัน: Expanded (FlexFit.tight) บังคับให้ child เต็มส่วนแบ่งของมันไม่ว่าขนาดธรรมชาติจะเป็นเท่าไหร่ — พิสูจน์: child กว้าง 50px ถูกยืดเป็น 300px Flexible (FlexFit.loose เป็น default) แค่จำกัดเพดาน child ไว้ที่ส่วนแบ่งของมัน ให้คงขนาดธรรมชาติถ้าเล็กกว่า — พิสูจน์: child 50px เดิมยังคงเป็น 50px | Core Layout Widgets |
| extension type | ตัวห่อแบบ zero-cost ตอน compile-time ของ Dart 3.3 — dart.dev: “extension types are static-only and compiled away at run time” extension type UserId(String value) { ... } เปิดให้ใช้เฉพาะสิ่งที่มันประกาศไว้จริง ๆ เท่านั้น การเรียก member จริงของ String บนมันคือ compile error undefined_method จริง แม้ id is String จะยังเป็น true ตอน runtime (ไม่มี object ห่ออยู่ให้เป็นอย่างอื่น) implements supertype ของ representation type เพื่อสืบทอด member มาฟรี ๆ | Dart Language Tour |
| Term | ความหมาย | บทเรียน |
|---|---|---|
.family | ตัวปรับแต่ง provider ของ Riverpod ที่ทำให้ provider รับ parameter ได้ สร้าง instance แยกต่อค่า parameter ที่ต่างกัน — ควรใช้คู่กับ .autoDispose เสมอ เว้นแต่ต้องการ cache state ของทุกค่า parameter ที่เป็นไปได้ไว้ตลอดไปจริง ๆ | Provider & Riverpod |
| federated plugin | plugin หลักของ Flutter ส่วนใหญ่วันนี้แบ่งเป็นสามส่วน: package ที่หันหน้าให้แอป, package platform-interface, และ package implementation หนึ่งตัวต่อแต่ละ platform — quote จาก docs.flutter.dev ตรง ๆ: “splitting the API of a plugin into a platform interface, independent platform implementations… and an app-facing interface” เรา depend package ที่หันหน้าให้แอปตัวเดียว แล้ว implementation ของ platform ที่ถูกต้องจะถูก resolve ให้เอง | Platform Channels |
final (class modifier) | ห้ามทั้ง extends และ implements นอก library ที่ประกาศ — error จริง: “can’t be extended outside of its library because it’s a final class” เข้มที่สุดในบรรดา modifier สี่ตัวที่ไม่ใช่ sealed: นอก library type นั้นสร้างและใช้ได้อย่างเดียว จะ subtype ด้วยวิธีไหนก็ไม่ได้เลย | Classes & Mixins |
| flavors / schemes | flavor ของ Android / scheme ของ iOS ให้ codebase เดียวสร้าง build dev/staging/prod ที่ config ต่างกันได้ (API endpoint, app ID, icon) โดยไม่ต้อง branch โค้ด — ใช้คู่กับ --dart-define เพื่อฉีดค่าต่อ environment เช่น flutter build apk --flavor prod --dart-define=API_URL=... | Build & Release |
flutter analyze / flutter_lints เทียบกับ very_good_analysis | flutter analyze รัน analyzer ตาม lint set ที่ตั้งไว้ flutter_lints (6.0.0 ค่า default) เพิ่มชุดเล็ก ๆ เฉพาะ Flutter ทับบน package:lints/recommended.yaml — พิสูจน์แล้วว่าจริง ๆ ไม่มี prefer_const_constructors ตัวทั่วไป (มีแค่ ..._in_immutables ที่แคบกว่า) ต่างจาก very_good_analysis (11.0.0 กฎ 237 บรรทัด) ที่มีให้เป็น default พร้อม baseline ที่เข้มกว่ามาก | Project & Tooling |
flutter doctor | คำสั่งแรกที่ควรรันเมื่อมีอะไรผิดปกติ — วินิจฉัย SDK, device toolchain ของทุก platform และ IDE plugin โดยระบุสิ่งที่ขาดชัดเจน (license Android ที่ยังไม่ accept, component ของ Xcode ที่ขาด) แทนที่จะบอกแค่ล้มเหลวลอย ๆ output จริงบน Mac ของคอร์สนี้: Flutter (Channel stable, 3.47.5, ...), Android toolchain (SDK 35.0.0), Xcode 26.2 | Project & Tooling |
FlutterError.onError | จับ error ที่ตัว Flutter framework เองโยนออกมา (build/layout/paint) — ช่องแรกในสองช่องที่จำเป็นสำหรับ crash reporting บน production; runZonedGuarded (ด้านล่าง) จับอีกครึ่งหนึ่ง คือ async error ที่ไม่ถูกจับ คู่กันนี้คือสิ่งที่คอร์สนี้พิสูจน์ว่าจับ “พื้นผิว error สองครึ่งที่ต่างกันของ Flutter” ได้ | Production Concerns |
| frame budget | เพื่อให้ได้ 60fps มีเวลา ~16.6ms ต่อเฟรมตลอด build→layout→paint→composite→raster; ที่ 120Hz (มือถือสมัยใหม่หลายรุ่น) เหลือครึ่งเดียวคือ ~8.3ms jank คือเฟรมที่พลาด budget นี้ — พิสูจน์ตลอดบทเรียน render-pipeline ว่ามาจากต้นทุนที่วัดได้จริง (layer Opacity ที่ถูกจอง, repaint ที่ไม่มี boundary) ไม่ใช่ความกังวลลอย ๆ | Render Pipeline |
Future | ค่าที่จะมีอยู่ในอนาคต — ผลของ operation แบบ async ที่แกะด้วย await Future.wait([...]) รันหลายตัวพร้อมกัน เสร็จเมื่อทุกตัวเสร็จ ตามลำดับ input ไม่ใช่ลำดับที่เสร็จก่อน; eagerError: true เสร็จทันทีที่ตัวแรกล้มเหลว แทนที่จะรอให้ทุก future นิ่งก่อน — พิสูจน์ด้วยเวลาจริง: default ~20ms เทียบกับ eagerError: true ที่ต่ำกว่า 30ms | Async in Dart |
FutureBuilder | แปลง Future เป็น AsyncSnapshot ที่ build อ่านได้แบบ synchronous โดยอ่านตามลำดับ connectionState → hasError → data บั๊กที่พบบ่อยที่สุด: สร้าง Future ข้างใน build — พิสูจน์ตรง ๆ ว่าทำให้งาน trigger ซ้ำและ snapshot รีเซ็ตเป็น waiting ทุกครั้งที่ rebuild แบบไม่เกี่ยวข้อง สร้างมันครั้งเดียว (initState/late final) แทน snapshot.hasError (error ที่ถูกจับจากต้นทาง) กับการ throw ข้างใน callback builder ของเราเอง พิสูจน์แล้วว่าเป็นโหมดล้มเหลวคนละแบบจริง ๆ — มีแค่แบบหลังเท่านั้นที่ทำให้ build พัง | Futures & Streams in the UI |
| FVM (Flutter Version Management) | เครื่องมือมาตรฐานของ community สำหรับ pin เวอร์ชัน Flutter SDK ต่อโปรเจกต์ (.fvmrc) แยกจากเวอร์ชันที่บังเอิญอยู่บน PATH — ไม่ใช่ส่วนหนึ่งของ SDK เอง เป็น CLI แยกที่จัดการ install ที่ cache ไว้หลายเวอร์ชัน คำตอบมาตรฐานของ “ทำไม build บนเครื่องฉันกับ CI ถึงต่างกัน” | Project & Tooling |
| Term | ความหมาย | บทเรียน |
|---|---|---|
GlobalKey | ระบุตัวตนของ widget ข้ามทั้งแอป เข้าถึงได้จากทุกที่โดยไม่ต้องมีความสัมพันธ์ ancestor/descendant — เครื่องมือมาตรฐานสำหรับเข้าถึง FormState จากนอก Form การใช้ GlobalKey เดียวกันกับ widget สองตัวพร้อมกันจะพังจริง พิสูจน์แล้ว: “Multiple widgets used the same GlobalKey… A GlobalKey can only be specified on one widget at a time.” ทรงพลังแต่แพง ควรใช้ ValueKey/UniqueKey ก่อน เว้นแต่ต้องการเข้าถึงข้าม tree จริง ๆ | Keys |
| golden test | test แบบ snapshot ที่เทียบพิกเซลที่ render กับไฟล์ .png อ้างอิงที่ commit ไว้ผ่าน matchesGoldenFile(...) — ทรงพลังสำหรับ design system แต่เปราะบางข้าม platform/เวอร์ชัน font; regenerate อย่างตั้งใจด้วย flutter test --update-goldens harness ของคอร์สนี้แนบ binary asset อ้างอิงผ่านระบบ fence ไม่ได้ จึงพิสูจน์อย่างตรงไปตรงมาใน scratch แทน ไม่ใช่ shared probe | Testing |
| go_router | package การนำทางแบบ declarative ตัวหลักที่สร้างบน Router API ของ Navigator 2.0 — คอร์สนี้ pin ไว้ที่ 18.0.1 (18.0.2 เป็นเวอร์ชันปัจจุบันบน pub.dev; การ pin คือภาพ snapshot ของ lockfile) context.go(path) ตั้ง location แล้วคำนวณ stack ใหม่จาก route table; URL deep-link ที่วางกลับมาสร้างหน้าจอเดิมได้บน web | Declarative Routing |
go_router_builder | สร้าง GoRouteData subclass ที่มี type ชัดเจนต่อ route จาก annotation @TypedGoRoute (เวอร์ชันปัจจุบัน: 4.5.0) — เปลี่ยน state.pathParameters['id']! ที่พิมพ์มือและ key เป็น string ให้กลายเป็น field ของ constructor จริงที่เช็คตอน compile-time | Declarative Routing |
GoRoute | รายการ route หนึ่งใน config GoRouter(routes: [...]): path (รองรับ path parameter แบบมี type เช่น :id) ที่ map ไปยัง builder ลำดับ route มีผล — path ตายตัวที่เจาะจงกว่า (/pets/new) ต้องอยู่ก่อน pattern ที่มี parameter ที่จะ match มันก่อนถ้าไม่จัดลำดับ (/pets/:id) | Declarative Routing |
| Term | ความหมาย | บทเรียน |
|---|---|---|
| Hero | บินย้าย widget ระหว่างสอง route เมื่อทั้งสองฝั่งใช้ tag เดียวกัน — พิสูจน์ทั้งสองทิศทางด้วย Navigator.push สองหน้าจอจริง | Animations |
| hot reload เทียบกับ hot restart | Hot reload (r) ฉีดโค้ดที่เปลี่ยนเข้าไปในแอปที่กำลังรันแล้ว rebuild widget tree — State ถูกรักษาไว้ เร็วกว่าวินาที Hot restart (R) ทิ้ง state ทั้งหมดแล้วเริ่มใหม่จาก main() Hot reload รัน build ใหม่ แต่ไม่รัน initState หรือ main ใหม่ และใช้กับการเปลี่ยนค่า const ใหม่หรือเปลี่ยนรูปร่าง class/enum ไม่ได้ — ถ้าเปลี่ยนแล้วดูเหมือนไม่มีผล คำตอบมักเป็น hot restart | Project & Tooling |
| Term | ความหมาย | บทเรียน |
|---|---|---|
| Impeller | renderer ของ Flutter ที่แทนที่การคอมไพล์ shader แบบruntimeของ Skia ด้วย shader และ pipeline state object (PSO) ที่คอมไพล์ไว้ล่วงหน้าตอน build — quote จาก docs.flutter.dev/perf/impeller ตรง ๆ: “It builds all pipeline state objects upfront. The engine controls caching and caches explicitly.” นี่แก้ jank คลาสสิก “animation ใหม่รันรอบแรกแล้วกระตุก” สถานะ ณ Flutter 3.47: เป็น renderer เดียวที่รองรับบน iOS (ไม่มี Skia สำรอง); เป็น default บน Android รุ่นใหม่ (รองรับ Vulkan) ตั้งแต่ 3.27; เป็น default บน macOS/Windows/Linux ตั้งแต่ 3.47 | Render Pipeline |
InheritedModel | rebuild dependent ตามaspect ไม่ใช่ตามค่าทั้งก้อนที่เปลี่ยน — MediaQuery สร้างอยู่บนมันจริง ๆ ทำให้ widget ที่อ่านแค่ MediaQuery.sizeOf(context) rebuild ตอนขนาดเปลี่ยน แต่ไม่ rebuild ตอน aspect อื่นที่ไม่เกี่ยวข้องเปลี่ยน เช่น padding ที่ไม่ขึ้นกับทิศทางจอ | InheritedWidget |
InheritedWidget | คำตอบในตัวของ Flutter สำหรับ prop-drilling: วางตัวหนึ่งไว้สูงในต้นไม้ แล้ว descendant ตัวไหนก็อ่านมันได้ตรง ๆ ผ่าน context.dependOnInheritedWidgetOfExactType<T>() (มักห่อด้วย static of(context)) ไม่ว่าจะลึกแค่ไหน พร้อม auto-rebuild เมื่ออัปเดต Theme, MediaQuery, Navigator, และ Provider ล้วนสร้างอยู่บนมัน — พิสูจน์ด้วย build-count test จริงระหว่าง dependent กับ non-dependent: มีแค่ dependent ที่ลงทะเบียนแล้วเท่านั้นที่ rebuild เมื่อ updateShouldNotify คืน true | InheritedWidget |
initState | รันครั้งเดียวตอน element ถูกสร้าง — จุดสำหรับ setup ครั้งเดียว (subscription, สร้าง controller) ไม่ใช่ side effect ที่ควรอยู่ใน build ยังอ่าน inherited widget ได้ไม่น่าเชื่อถือ (ใช้ didChangeDependencies แทน) field ที่เป็น late ที่ไม่ถูก assign หลัง initState จะโยน LateInitializationError จริง | setState & Lifecycle |
integration_test | มากับ SDK เลย (ไม่ใช่ dependency แยกจาก pub.dev) รันแอปที่ compile จริงบน device/emulator จริงผ่าน IntegrationTestWidgetsFlutterBinding.ensureInitialized() แล้วใช้ API tester.tap/pump/expect แบบเดียวกับ widget test — แต่กับแอปที่รันจริง สภาพแวดล้อมของคอร์สนี้ไม่มี device ให้รัน หัวข้อนี้จึงอ้างอิงจาก docs (คู่มือ integration testing ของ docs.flutter.dev) ไม่ใช่การสาธิตจริง | Testing |
interface (class modifier) | ห้าม extends นอก library ที่ประกาศ แต่อนุญาต implements — error จริง: “can’t be extended outside of its library because it’s an interface class” ทางแก้คือสิ่งที่ modifier นี้มีไว้เพื่อสิ่งนั้นพอดี: implement แทน extend ซึ่ง compile ผ่านสะอาด | Classes & Mixins |
IntrinsicHeight | บังคับให้ widget ปรับขนาดตามความสูง “ธรรมชาติ” ของ child โดยรันการวัดพิเศษอีกรอบจริงก่อน (dry layout เพื่อหา intrinsic size แล้วค่อย layout จริง) — พิสูจน์แล้วว่าแพงกว่า layout ธรรมดาจริง ไม่ใช่แค่ทางเลือกด้านสไตล์ | Constraints & Layout |
Isolate.run / Isolate.spawn | Isolate.run (Dart 2.19+) spawn isolate, รัน closure, คืนผล แล้วปิด isolate — ทั้งหมดในคำเรียกเดียว นี่คือ primitive ตัวเดียวกับที่ compute() ของ Flutter ห่ออยู่บน native เป๊ะ (พิสูจน์ว่าเท่ากันจาก source ของ SDK) Isolate.spawn + SendPort/ReceivePort ใช้กับ worker isolate ที่อยู่นานคุยกันซ้ำ ๆ แทนที่จะ spawn isolate ใหม่ทุกครั้ง | Async in Dart |
| Term | ความหมาย | บทเรียน |
|---|---|---|
| JIT เทียบกับ AOT | JIT (Just-In-Time ใช้ใน debug) คอมไพล์ Dart ระหว่างรัน ซึ่งเป็นสิ่งที่ทำให้ hot reload เป็นไปได้ AOT (Ahead-Of-Time ใช้ใน profile และ release) คอมไพล์เป็น native machine code ก่อนส่งออก — startup เร็ว performance คาดเดาได้ แต่ไม่มี hot reload Tree-shaking (โค้ดที่ไม่ใช้และ glyph icon ที่ไม่ถูกอ้างอิงถูกตัดออก) เกิดขึ้นใน AOT build | Build & Release |
| Term | ความหมาย | บทเรียน |
|---|---|---|
| key | บอก Flutter ว่า “identity ของ element นี้คือ X ไม่ใช่แค่ตำแหน่งของมัน” — ถ้าไม่มี Flutter จะจับคู่ตาม type+ตำแหน่ง แล้ว list ของ stateful widget ที่ถูกสลับลำดับจะทำให้ State ผิดติดอยู่กับแถวผิด (พิสูจน์ด้วย checkbox test จริง: check “ค้าง” อยู่ตำแหน่ง 0 หลังสลับ แทนที่จะตามป้ายชื่อของมันไป) จำเป็นสำหรับ list ของ stateful widget ที่ลำดับ/สมาชิกเปลี่ยนได้ แทบไม่จำเป็นสำหรับ list แบบ stateless ดู ValueKey, UniqueKey, GlobalKey, PageStorageKey | Keys |
kIsWeb | bool ที่รู้ตอน compile-time เป็น true เฉพาะบน web target — ตัวป้องกันจริงสำหรับแยกโค้ดที่แตะ dart:io ออกจาก web build เพราะ dart:io เอง compile ผ่าน (เป็น stub) แต่โยน UnsupportedError ตอน runtime บน web แทนที่จะ build ไม่ผ่าน | Project & Tooling |
kReleaseMode | flag bool ที่รู้ตอน compile-time จาก package:flutter/foundation.dart เป็น true เฉพาะใน release build — ใช้ภายใน source ของ compute() เองเพื่อเลือก debug label ที่สั้นกว่า ('compute' ใน release เทียบกับ toString() เต็มของ callback ใน debug) และโดยทั่วไปคือตัวป้องกันมาตรฐานสำหรับตัด diagnostic/assert ที่ใช้เฉพาะ debug ออกจากสิ่งที่ส่งจริง | Isolates & compute() |
| Term | ความหมาย | บทเรียน |
|---|---|---|
l10n / gen-l10n | pipeline localization ทางการของ Flutter: ไฟล์ .arb + l10n.yaml พร้อม generate: true ใต้ flutter: ใน pubspec.yaml และเพิ่ม flutter_localizations (dependency ของ SDK: sdk: flutter ไม่ใช่ pub.dev) flutter gen-l10n เขียน app_localizations.dart ที่มี type ชัดเจนพร้อม class ต่อ locale — รันจริงกับ SDK Flutter 3.47.5 ของคอร์สนี้ ได้ output ที่ generate จริง | App Scaffolding |
late | สัญญาว่า “จะไม่เป็น null ตอนที่มีใครอ่านมัน” โดยไม่ต้องมีค่าเริ่มต้นตอนประกาศ — มีประโยชน์กับค่าที่ตั้งใน initState อ่านก่อน assign จะโยน LateInitializationError จริง late ที่ใช้ผิดคือ ! ในคราบปลอมตัว: มันแลกการรับประกันตอน compile-time กับตอน runtime ดังนั้นควรใช้เท่าที่จำเป็นเท่านั้น | Dart Language Tour |
LayoutBuilder เทียบกับ MediaQuery.of | ทั้งคู่ให้ widget ตอบสนองต่อขนาดได้ แต่ขอบเขต rebuild ต่างกันจริง — พิสูจน์ตรง ๆ: MediaQuery.of(context) (หรือ .sizeOf) rebuild ทุกครั้งที่ MediaQueryData โดยรอบเปลี่ยนที่ไหนก็ได้ในแอป (หมุนจอ, text-scale เปลี่ยน) ส่วน LayoutBuilder rebuild เฉพาะตอนconstraint ที่รับเข้ามาของมันเองเปลี่ยน ซึ่งจำกัดขอบเขตอยู่แค่ตำแหน่งที่มันอยู่ในต้นไม้ ไม่ใช่ทั้งแอป | Build & BuildContext |
| layer / layer tree | ผลลัพธ์จากขั้น compositing ของเฟส paint — ไม่ใช่ทุก render object จะมี layer ของตัวเอง การห่อ subtree ด้วย RepaintBoundary คือสิ่งที่จองมันขึ้นมา debugDumpLayerTree() คือ dump จริงที่ตรวจสอบได้ของ layer tree ที่มีชีวิตอยู่จริง (TransformLayer → OpacityLayer → PictureLayer) ไม่ใช่คำอธิบายลอย ๆ — แม้แต่ Center(child: Text(...)) ธรรมดาก็มี OpacityLayer จากกลไก route-transition อยู่แล้ว ก่อนที่โค้ดแอปจะแตะ Opacity เลย | Render Pipeline |
| Term | ความหมาย | บทเรียน |
|---|---|---|
การแยก material_ui / cupertino_ui | การมีส่วนร่วม (contribution) ใน package:flutter/material.dart และ cupertino.dart ข้างใน core SDK ถูกแช่แข็งตั้งแต่ Flutter 3.44 (quote ตรง ๆ จาก breaking-changes note ของ docs.flutter.dev) — งาน Material/Cupertino ใหม่ไปอยู่ที่ package material_ui/cupertino_ui แบบแยกเวอร์ชันอิสระบน pub.dev แทน (ทั้งคู่เกิน 1.0 แล้ว ณ 3.47) package:flutter/material.dart ยังใช้งานได้ ยังมากับทุกการติดตั้ง และยังไม่ถูก deprecate อย่างเป็นทางการ ณ 3.47 — แค่ถูกแช่แข็งเท่านั้น dart fix --apply --code=migrate_design_widgets คือการ migrate อัตโนมัติทางการเมื่อพร้อม | App Scaffolding |
MaterialApp.router | constructor variant ของ MaterialApp สำหรับการนำทางแบบ declarative (Navigator 2.0) — รับ routerConfig: (instance ของ GoRouter ในคอร์สนี้) แทน home:/routes: ให้ router (ไม่ใช่ MaterialApp เอง) เป็นเจ้าของ route stack ทั้งหมด | App Scaffolding |
MediaQuery / SafeArea | MediaQuery.of(context) เปิดเผยข้อมูลจอโดยรอบ (ขนาด, padding, text scale, ทิศทางจอ) — สร้างอยู่บน InheritedModel ดังนั้น .sizeOf(context) เดี่ยว ๆ จะจำกัดขอบเขต rebuild ให้เฉพาะการเปลี่ยนขนาด SafeArea อ่าน padding ของ MediaQuery เพื่อเลี่ยง notch/system bar ให้อัตโนมัติ | App Scaffolding |
MethodChannel | ท่อส่งข้อความแบบ async ที่มี type ไปยังโค้ด native: invokeMethod('name') คืน Future ที่เสร็จพร้อมคำตอบจาก native ที่ serialize ผ่าน MethodCodec ถ้าไม่มีการ implement ฝั่ง native เลย จะโยนข้อความ MissingPluginException จริงเป๊ะ ๆ — flutter_test มี mock messenger (setMockMethodCallHandler) ที่ round-trip ผ่าน codec จริง ดังนั้น test ที่ mock ไว้พิสูจน์การ (de)serialize จริง ไม่ใช่แค่ stub | Platform Channels |
mixin class | class ที่ใช้ได้ทั้งเป็น class Foo และ with Foo (mixin) — ประกาศ generative constructor ที่ไม่ trivial ไม่ได้ เพราะ field ของ mixin จะถูกต่อกิ่งเข้ากับ class ไหนก็ตามที่ apply มัน ซึ่ง constructor จะไม่มีความหมาย error จริง: mixin_class_declares_non_trivial_generative_constructor | Classes & Mixins |
mocktail (when / verify / registerFallbackValue) | mocktail (1.0.5) แทนที่ collaborator ปลอมเข้าไปใน widget ที่ทดสอบ: when(() => repo.fetch()).thenAnswer((_) async => data) stub การเรียก, verify(() => repo.save(any())).called(1) assert ว่าถูกเรียกอย่างไรแบบเป๊ะ ๆ registerFallbackValue(SomeFake()) มีไว้เพราะ any() ต้องการ instance จริงที่ไม่ใช่ null ของ type ที่กำหนดเองข้างในภายใต้ sound null safety — Fake (ไม่เคยถูกเรียกจริง แค่ identity ของ type มันสำคัญ) คือคำตอบมาตรฐานเมื่อ class จริงสร้างตรง ๆ ไม่ได้ ลืมมันจะโยน error จริงที่มีชื่อชัดเจน | Testing |
mounted | getter จริงที่ไม่เป็น null บน State เป็น true จนกว่า dispose() จะรัน ใช้ป้องกัน setState หลัง await — setState หลัง await ที่ไม่มีการ์ดบน widget ที่ถูก dispose แล้วจะโยน error setState() called after dispose() จริงเป๊ะ ๆ if (!mounted) return; ก่อน setState เปลี่ยนการพังให้กลายเป็น no-op ที่ไม่มีอันตราย พิสูจน์ตรง ๆ | setState & Lifecycle |
| Term | ความหมาย | บทเรียน |
|---|---|---|
NavigationBar เทียบกับ NavigationRail | การนำทางแบบ Material ที่ปรับตัวได้: NavigationBar (แถบล่าง) สำหรับความกว้างแคบ, NavigationRail (แถบข้าง) สำหรับความกว้างมาก — พิสูจน์ที่สองความกว้างด้วย widget test จริง เป็น pattern มาตรฐานให้หน้าจอเดียวปรับตัวระหว่างมือถือกับ tablet/desktop | App Scaffolding |
| Navigator / route stack | API แบบ imperative (Navigator 1.0): push/pop/pushReplacement/pushNamedAndRemoveUntil จัดการ stack ของ route ตรง ๆ Navigator.of(context) เดินขึ้นต้นไม้หา Navigator ancestor ที่ใกล้ที่สุดแล้วโยน error จริงชื่อชัดเจนถ้าไม่มี — รูปแบบความล้มเหลว “ผิด scope” แบบเดียวกับ Scaffold.of() rootNavigator: true เจาะจงไปที่ Navigator ชั้นนอกสุดแทนตัวที่ซ้อนอยู่ข้างใน | Navigator & Routes |
Navigator 2.0 (Router / RouterDelegate / RouteInformationParser) | ระบบการนำทางแบบ declarative ที่อยู่ใต้ go_router: Router คือตัวจัดส่งที่คอยฟังข้อมูลการนำทาง (deep link, URL browser); RouteInformationParser แปลงข้อมูลดิบนั้นให้เป็น app state ที่มี type; RouterDelegate สร้าง Navigator/Page list จริงจาก state นั้นแล้วรายงานการ pop กลับ go_router คือ คู่ RouterDelegate/RouteInformationParser ที่ package เขียนไว้ให้ครั้งเดียว จึงแค่ config GoRoute แทนที่จะเขียน delegate เอง | Navigator & Routes |
null-safety operators (?. / ?? / ??= / !) | ?. — เข้าถึงเฉพาะตอนไม่เป็น null ไม่งั้นทั้ง expression เป็น null ?? — ค่า fallback เมื่อฝั่งซ้ายเป็น null ??= — assign เฉพาะตอนยังเป็น null อยู่ ! — ยืนยันว่าไม่ใช่ null โยน error ตอน runtime ถ้าผิด เป็น operator ตัวเดียวที่ควรระวังจริงจัง พิสูจน์ด้วย runtime crash จริงตอน assertion ผิด | Null Safety |
| Term | ความหมาย | บทเรียน |
|---|---|---|
Opacity (ต้นทุนของ layer) | doc comment ของ widget เองบอกว่าค่า 0/1 ใช้ “fast path” — อ่าน source จริงของ RenderOpacity แสดงกฎที่แม่นยำกว่า: alwaysNeedsCompositing => child != null && _alpha > 0 เป็นจริงกับ alpha ที่ไม่เท่ากับศูนย์ทุกค่า รวมถึง 255 (opacity 1.0) พิสูจน์ตรง ๆ ด้วยการตรวจ layer tree ที่มีชีวิตจริง: มีแค่ opacity: 0.0 เท่านั้นที่ทำให้ layer compositing ของ widget เองหายไปจริง 1.0 ยังจอง OpacityLayer จริงอยู่ดี แค่ composite ได้ถูกกว่ามาก | Render Pipeline |
| Term | ความหมาย | บทเรียน |
|---|---|---|
PageStorageKey | รักษาตำแหน่ง scroll ข้าม scrollable ที่ถูกรื้อทิ้งแล้วสร้างใหม่ตั้งแต่ต้น (เช่น สลับออกจาก tab ที่ไม่ keep child ไว้ แล้วสลับกลับมา) — อ่านผ่าน PageStorage/PageStorageBucket โดยรอบ พิสูจน์ตรง ๆ ว่ากู้ offset เดิมกลับมาได้เป๊ะ ในขณะที่ rebuild ที่ไม่มี key จะรีเซ็ตเป็นศูนย์ | Keys |
| pattern matching (switch expression, guard, destructuring) | pattern ของ Dart 3.0 ขยาย destructuring ให้ไกลกว่า final (a, b) = pair มาเป็นการ match ตามรูปร่างของค่า — object pattern (Circle(:final radius)), list pattern ที่มี rest element, map pattern ผ่าน if (json case {...}) และ guard แบบ relational (Circle(radius: > 10)) หรือ when switch แบบ expression เหนือ type ที่เป็น sealed หรือ enum ถูก analyzer เช็ค exhaustiveness ให้ (ดูหัวข้อนั้น) โดยไม่ต้องมี default | Dart Language Tour |
| Pigeon | สร้าง Dart API ที่มีtype ชัดเจนพร้อม interface ฝั่ง native (Kotlin, Swift) ที่ตรงกัน จากไฟล์ definition Dart ไฟล์เดียว (เวอร์ชันปัจจุบัน: 29.0.4) — การเปลี่ยนชื่อหรือ type จะ compile ไม่ผ่านทุกที่ที่ใช้ แทนที่จะพังแบบเงียบ ๆ ที่ขอบเขต MethodChannel ที่ใช้ string รันครั้งเดียว (dart run pigeon --input ...) เป็นคำสั่ง dev มีแค่ output .g.dart/.g.kt/.g.swift ที่ generate แล้วเท่านั้นที่ไปกับแอป | Platform Channels |
PlatformException / MissingPluginException | PlatformException คือ error จริงที่มีโครงสร้าง (code, message) ที่ handler ฝั่ง native โยนออกมาโดยตั้งใจ ปรากฏใน Dart เป็น exception ที่จับได้ MissingPluginException คือสิ่งที่ได้เมื่อไม่มีการ implement native เลยสำหรับ channel นั้น — ข้อความ toString() ที่แน่นอน ('MissingPluginException($message)') พิสูจน์แล้วว่าเกิดจริงและเป็นข้อความเป๊ะ ๆ ที่ bug report “หา plugin ไม่เจอ” จริงทุกอันมี | Platform Channels |
PopScope | แทนที่ WillPopScope ที่ deprecated แล้วสำหรับสกัดกั้น back gesture/ปุ่ม — ตามคู่มือ predictive-back ของ docs.flutter.dev มันคือสิ่งที่ “รองรับ predictive back gesture” ซึ่ง WillPopScope ทำไม่ได้ | Navigator & Routes |
predictive back / PredictiveBackPageTransitionsBuilder | ตั้งแต่ Flutter 3.38 transition หน้าจอ default บน Android คือ PredictiveBackPageTransitionsBuilder (ชื่อ class จริง — เคยมี typo ในคอร์สที่เขียนขาด “s” หลัง “Transitions” ครั้งหนึ่ง; การสะกดนั้นไม่มีอยู่จริงใน SDK) ตาม back gesture ของผู้ใช้ แสดงตัวอย่างหน้าจอปลายทางข้างใต้ระหว่างลาก ได้อัตโนมัติผ่าน MaterialPageRoute การ preview ระดับ OS ยังต้องการ android:enableOnBackInvokedCallback="true" ใน manifest เพิ่มเติม | Navigator & Routes |
| Provider (package) | ห่อ boilerplate ของการเขียน InheritedWidget มือด้วย API ที่สะอาด มักคู่กับ ChangeNotifier: model เรียก notifyListeners(), ChangeNotifierProvider วางมันไว้ในต้นไม้ แล้ว context.watch<T>()/context.read<T>() อ่านมันแบบมี (หรือไม่มี) การผูก rebuild | Provider & Riverpod |
ProviderScope | widget ที่สร้างและเปิดเผย ProviderContainer ของ Riverpod — ไม่ใช่ตัวเลือก: ทุก ref.watch/ref.read ต้องการมันอยู่ข้างบนที่ไหนสักแห่ง ข้ามมันไปจะโยน error จริง StateError: "Bad state: No ProviderScope found" ทันทีที่ ConsumerWidget พยายามอ่าน provider วางครั้งเดียวที่ root ของแอป ห่อ runApp() ไว้ | Provider & Riverpod |
pubspec.yaml | manifest ของโปรเจกต์: dependencies: (ส่งจริง), dev_dependencies: (build/test เท่านั้น เช่น flutter_lints), environment: sdk: (ข้อจำกัด Dart SDK, ^3.13.0 สำหรับ Flutter 3.47 ของคอร์สนี้) และ flutter: assets:/fonts: (ทรัพยากรที่แนบไปด้วย — รายการที่ขาดตรงนี้คือบั๊ก “หา asset ไม่เจอ” คลาสสิก) | Project & Tooling |
| Term | ความหมาย | บทเรียน |
|---|---|---|
| record | โครงสร้างข้อมูลของ Dart 3.0 แบบไม่มีชื่อ ไม่เปลี่ยนแปลงได้ (immutable) และมี type ตามโครงสร้าง — (double, double) หรือ ({String name, int age}) — สำหรับคืนค่าหลายค่าโดยไม่ต้องสร้าง class เลย เทียบด้วย == แบบตามโครงสร้างเสมอ; record ที่เป็น const จะถูก canonicalize เพิ่ม (identical) พิสูจน์ตรง ๆ เหมือน widget const เป๊ะ typedef ตั้งชื่อรูปร่าง record ได้; field แบบ positional อ่านด้วย .$1/.$2 | Dart Language Tour |
redirect / refreshListenable | redirect รันก่อน route ของ go_router จะ build และส่งผู้ใช้ไปที่อื่นได้ (คืน path หรือ null เพื่อไปต่อ) — ที่ที่ควรใช้กับ auth guard โดยธรรมชาติ ตามลำพังมันจะ re-evaluate แค่ตอนมีการนำทางเกิดขึ้น refreshListenable (Listenable ใด ๆ มักเป็น ChangeNotifier) ปิดช่องว่างนั้น ทำให้ router รัน redirect ใหม่ทุกครั้งที่ listenable notify โดยไม่ต้องมีการเรียกนำทางของตัวเองเลย — พิสูจน์ตรง ๆ: notifyListeners() จากภายนอกเพียว ๆ ไม่มี go()/push() เลย ก็ยังพาแอปจากหน้า login ไปหน้าแรกได้ | Declarative Routing |
ref.watch / ref.read / ref.listen | สามวิธีของ Riverpod ในการโต้ตอบกับ provider จากข้างใน provider อื่นหรือ widget: ref.watch subscribe แล้ว rebuild เมื่อเปลี่ยน; ref.read อ่านครั้งเดียว ไม่ subscribe (มักอยู่ใน callback); ref.listen รัน side effect ตอนเปลี่ยนโดยไม่ทำให้ rebuild เอง | Provider & Riverpod |
RenderFlex overflowed | ข้อความ error จริงเป๊ะ ๆ เมื่อ Row/Column ได้รับเนื้อหาเกินพื้นที่ที่มี — Row/Column ไม่มีวันย่อ child ให้พอดี จำนวนพิกเซลที่รายงานคำนวณจริง ไม่ใช่ตัวเลขลอย ๆ และตรงกับคณิตศาสตร์การ overflow จริง (ผลรวม child − พื้นที่ที่มี) พิสูจน์ตรง ๆ: กล่องกว้าง 300px สองกล่องใน Row กว้าง 400px รายงาน “overflowed by 200 pixels” Expanded/Flexible (หรือ scrollable) คือทางแก้จริง | Constraints & Layout |
| RenderObject / render tree | ต้นไม้ต้นที่สามในสามต้นไม้ — ทำ layout, paint, hit-test จริง แพง Flutter จึงนำกลับมาใช้ใหม่อย่างจริงจัง ต่างจาก widget tree ที่ถูกและ rebuild ตลอดเวลา RenderFlex, RenderParagraph, RenderOpacity ล้วนเป็น RenderObject จริงที่ debugDumpRenderTree() พิมพ์ state ที่มีชีวิตจริงออกมาให้ดู | Widgets and the Tree |
RepaintBoundary | ให้ subtree มีcompositing layer เป็นของตัวเอง แยก repaint ของมันออกจาก sibling ที่ไม่เกี่ยวข้องซึ่งไม่งั้นจะใช้ canvas ร่วมกัน — พิสูจน์ด้วยตัวนับ paint จริง: ถ้าไม่มี boundary ตัวนับ paint ของ sibling ที่นิ่งอยู่จะขยับตามตัวที่กำลัง animate (1→2 ทั้งที่ไม่มีอะไรเปลี่ยนจริง); มี boundary แล้วตัวนับของ sibling ที่นิ่งจะค้างที่ 1 | Render Pipeline |
runZonedGuarded | วิธีเดียวที่เชื่อถือได้ในการจับ error ที่ throw จาก callback ที่ไม่มีใคร await (Timer, Future แบบ fire-and-forget, stream listener) — พิสูจน์ตรง ๆ: try/catch ธรรมดารอบโค้ดที่ตั้งเวลาให้ callback นั้น ไม่เห็น error ที่เกิดทีหลังเลยจริง ๆ ในขณะที่ error handler ของ zone ที่ตั้งผ่าน runZonedGuarded เห็น นี่คือเหตุผลที่ main() บน production ห่อ runApp() ไว้ด้วยมัน คู่กับ FlutterError.onError สำหรับอีกครึ่งของพื้นผิว error ที่เป็นของ framework เอง | Async in Dart |
| Term | ความหมาย | บทเรียน |
|---|---|---|
ScaffoldMessenger | เป็นเจ้าของการแสดง SnackBar แยกออกจาก Scaffold ตัวใดตัวหนึ่ง — เข้าถึงแบบเดียวกับ Scaffold.of() และพังแบบ “ผิด scope” เหมือนกันถ้า context ที่ใช้อยู่เหนือ Scaffold/ScaffoldMessenger ที่ตั้งใจจะเข้าถึง | App Scaffolding |
Scaffold.of() (การพังเพราะผิด scope) | เพราะ .of(context) ค้นหาขึ้นไปข้างบน การเรียก Scaffold.of(context) ข้างใน build เดียวกับที่สร้าง Scaffold ใช้ context ที่อยู่เหนือมัน ไม่ใช่ใต้ — โยน error จริงชื่อชัดเจน “Scaffold.of() called with a context that does not contain a Scaffold.” Builder (สร้าง context ที่เป็น descendant จริง) คือทางแก้มาตรฐาน พิสูจน์แล้วว่ากำจัดการพังได้ Theme.of/Navigator.of พังแบบเดียวกันด้วยเหตุผลเดียวกัน | Build & BuildContext |
scrollCacheExtent | API ปัจจุบันสำหรับควบคุมว่า Flutter จะ pre-build list item ล่วงหน้าแค่ไหน (เป็นพิกเซล หรือ multiplier เทียบกับ viewport) นอกขอบ viewport ที่มองเห็น — แทนที่ parameter cacheExtent: double ที่ deprecated แล้ว (ถูก flag โดย flutter analyze บน SDK นี้) พิสูจน์แล้วว่าควบคุมจำนวน build ได้จริง: cache default build ไป 11/1000 item ในการรันที่วัดจริง; ScrollCacheExtent.pixels(1000) build ไป 26 | Slivers & Scrolling |
sealed (class modifier) | ปิดทั้ง extends และ implements นอก library ที่ประกาศ — ทุก subtype ต้องอยู่ใน library เดียวกัน ซึ่งเป็นสิ่งที่ทำให้การเช็ค exhaustiveness ของ switch เหนือมันมั่นคง (analyzer นับทุกกรณีได้) error จริง: “can’t be extended, implemented, or mixed in outside of its library because it’s a sealed class” รูปแบบมาตรฐานสำหรับ union ของ event/state ใน BLoC | Classes & Mixins |
.select() | การอ่าน provider ของ Riverpod ที่ rebuild เฉพาะตอนที่ค่าที่ derive มาเจาะจงเปลี่ยน ไม่ใช่ state ของ provider ทั้งก้อน — เทียบเท่ากับ aspect ของ InheritedModel หรือ buildWhen ของ BLoC ใน Riverpod | Provider & Riverpod |
| Semantics | ชั้น accessibility ที่อ่านคู่ไปกับ render tree — RenderObject ที่โต้ตอบได้หรือมีข้อมูลใด ๆ สามารถแนบ SemanticsNode ได้ (เห็นได้ตรง ๆ ใน debugDumpRenderTree() จริง เช่น semantics node: SemanticsNode#4 สำหรับ Text ธรรมดา) widget ที่ซ่อนด้วย Opacity(opacity: 0.0) เพียว ๆ ยังโต้ตอบได้อยู่ถ้าไม่จัดการ IgnorePointer/alwaysIncludeSemantics เอง | Render Pipeline |
setState | ทำแค่สิ่งเล็ก ๆ อย่างเดียว: รัน callback (mutate field ข้างในมัน) แล้วทำเครื่องหมาย element ของ widget นี้เป็นdirtyสำหรับเฟรมถัดไป — ไม่ synchronous ไม่ใช่ “วาดจอใหม่” การเรียกมันระหว่างที่กำลัง build อยู่ อนุญาตเฉพาะกับ descendant ของ widget ที่กำลัง build อยู่เท่านั้น (parent build ก่อน child เสมอ ดังนั้น framework รับประกันได้ว่ามันจะถูกเยี่ยมรอบนี้แน่) การเอื้อมไปด้านข้างหรือขึ้นบนจะถูกปฏิเสธ | setState & Lifecycle |
Sliver / SliverList / SliverAppBar | sliver คือพื้นที่ scroll ที่ต่อรองขอบเขตที่มองเห็นกับ viewport แบบ lazy แทนที่จะครองกล่องตายตัวเหมือน widget ปกติ ListView ห่อ sliver ตัวเดียวไว้; CustomScrollView ประกอบหลายตัวเข้าด้วยกัน (SliverAppBar สำหรับ header ที่ยุบได้, SliverList/SliverGrid สำหรับเนื้อหาแบบ lazy, SliverToBoxAdapter สำหรับใส่ widget ธรรมดาเข้าไป) | Slivers & Scrolling |
SliverFillRemaining | ข้างใน CustomScrollView มันครองพื้นที่ viewport ที่เหลือหลังจาก sliver ตัวอื่น — เทียบเท่ากับ Expanded ใน Column สำหรับ sliver; hasScrollBody: false สำหรับเนื้อหาที่ไม่ควร scroll เอง (ข้อความ empty-state, ปุ่มที่ยึดอยู่ด้านล่าง) | Slivers & Scrolling |
StatefulShellRoute.indexedStack | ให้แต่ละ branch ของ shell แบบ bottom-navigation มี Navigator ของตัวเองที่ซ้อนอยู่ เก็บทุก branch ให้มีชีวิตพร้อมกันใน IndexedStack — พิสูจน์ตรง ๆ: push หน้ารายละเอียดใน tab A สลับไป tab B แล้วกลับมา หน้ารายละเอียดยังอยู่ ShellRoute ธรรมดาใช้ Navigator เดียว ร่วมกันทุก branch แทน — สลับ tab ไม่ push/pop อะไรเลย แค่สลับว่า route ระดับบนสุดตัวไหน active ทำให้ back-stack ต่อ branch หายไป | Declarative Routing |
StatefulWidget / State | StatefulWidget ยังคง immutable เหมือน widget ทั่วไป ข้อมูลที่เปลี่ยนแปลงได้อยู่ใน object State แยกต่างหากที่ element ที่อยู่ต่อเนื่องถืออยู่ — พิสูจน์ตรง ๆ: parent rebuild ด้วย key/type เดิม ยังคง State instance เดิมเป๊ะ (field เดิม) ในขณะที่ key หรือ type เปลี่ยนจะสร้างตัวใหม่จากศูนย์ | Stateless vs. Stateful |
StateNotifier / StateProvider (legacy) | primitive สำหรับ state ที่เปลี่ยนแปลงได้ของ Riverpod ก่อนเวอร์ชัน 3.0 — migration guide ระบุว่าเป็น legacy ใน Riverpod 3 ถูกแทนที่ด้วย Notifier/AsyncNotifier (ที่คอร์สนี้ใช้ตลอด) ยังคงอยู่เพื่อความเข้ากันได้ตอน migrate ไม่ใช่จุดเริ่มต้นที่แนะนำสำหรับโค้ดใหม่ | Provider & Riverpod |
StatelessWidget | UI ที่เป็น pure function ของ input จาก constructor เท่านั้น — ไม่มีข้อมูลที่เปลี่ยนแปลงได้เป็นของตัวเอง มีแค่ build เท่านั้น ใช้กับสิ่งที่ไม่เปลี่ยนแปลงเองตามเวลา | Stateless vs. Stateful |
| Term | ความหมาย | บทเรียน |
|---|---|---|
testWidgets variant | variant: someTestVariant รัน test body เดิม หนึ่งครั้งต่อค่าใน TestVariant — พิสูจน์ด้วย ValueVariant<String>({'en','th','ja'}) จริง: เรียก testWidgets ครั้งเดียว ได้ผลลัพธ์การรัน test แยกกันสามครั้ง TargetPlatformVariant.all() คือ variant สำเร็จรูปสำหรับรัน widget test เดียวข้ามทุก TargetPlatform โดยไม่ต้องเขียน loop เอง | Testing |
Ticker / TickerProvider | Ticker ยิง callback หนึ่งครั้งต่อเฟรมของ animation; SingleTickerProviderStateMixin/TickerProviderStateMixin ให้ vsync: ที่ AnimationController ต้องการ Ticker ที่ยัง active อยู่แล้วไม่ถูก dispose จะโยน assert จริงที่ทำงานเฉพาะ debug mode ตอน element ถูกเอาออก — ต้นทุนที่ controller.dispose() มีไว้เพื่อหลีกเลี่ยง | Animations |
Timer.run (งานใน event queue) | Future(() {}) เปล่า ๆ แอบเป็นงานใน event queue ที่ใช้ Timer.run เป็นฐาน ไม่ใช่ microtask — พิสูจน์ตรง ๆ ด้วยลำดับจริง: scheduleMicrotask/await ที่ต่อเนื่องเป็น microtask จริงที่ทำงานจนหมดก่อนเสมอ ก่อนที่งานที่ใช้ Timer จะรัน ไม่ว่าจะเขียนลำดับไหนก็ตาม | Async in Dart |
TransferableTypedData | ส่งไบต์ดิบระหว่าง isolate โดยไม่ copy — เปลี่ยนความเป็นเจ้าของแทน (ฝั่งส่งใช้ข้อมูลของตัวเองไม่ได้อีกต่อไป; materialize() ฝั่งส่งโยน error หลัง transfer) ยืนยันจาก api.dart.dev: “can be moved between isolates, so sending it… will only take constant time.” วัดบน Mac เครื่องนี้ที่ 10MB ชัยชนะเหนือการ copy ของ compute() ธรรมดายังเล็ก (overhead การ spawn isolate คงที่ครอบงำที่ขนาดนี้) — ชัยชนะจริงจะเห็นที่ payload ใหญ่กว่านี้มากหรือเมื่อส่งผ่านหลาย isolate ต่อกัน | Isolates & compute() |
| สามต้นไม้ | Widget tree (คำอธิบายที่ไม่เปลี่ยนแปลง rebuild ตลอดเวลา ทิ้งได้ ถูก), Element tree (instance ที่มีชีวิตอยู่ต่อเนื่อง — ถือ State ตัดสินใจว่าจะอัปเดตในที่หรือรื้อทิ้ง), Render tree (RenderObject ที่ layout และ paint จริง — แพง นำกลับมาใช้ใหม่อย่างจริงจัง) type เดิม + ไม่มี key ที่ตำแหน่งเดิมในต้นไม้จะใช้ element เดิมซ้ำ (createElement รันครั้งเดียว didUpdateWidget ทุกครั้งที่ rebuild พิสูจน์ด้วยการ spy); key ที่ต่างกันที่ตำแหน่งเดิมจะรื้อทิ้งแล้วสร้างใหม่ (createElement ใหม่) | Widgets and the Tree |
| Term | ความหมาย | บทเรียน |
|---|---|---|
unawaited_futures | lint ที่จับการเรียก async แบบ fire-and-forget (ไม่ await, ไม่เก็บ Future ไว้) — พิสูจน์แล้วว่าไม่อยู่ใน flutter_lints/flutter.yaml แบบ default ของคอร์สนี้ (มีแค่ใน template ที่เข้มกว่าเท่านั้น) error ข้างใน Future ที่ไม่ถูก await จะกลายเป็นสิ่งที่ไม่ถูกจับ มองเห็นได้แค่ runZonedGuarded ไม่มีวันถูก try/catch รอบข้างเห็น unawaited(fn()) จาก dart:async คือวิธีจริงที่ไม่มีผลตอน runtime ในการบอกว่าการ fire-and-forget นั้นตั้งใจ | Async in Dart |
UniqueKey | บังคับให้ Flutter ปฏิบัติต่อ widget เป็น identity ใหม่ทั้งหมดตามต้องการ ทิ้ง element/State เก่าโดยตั้งใจ — พิสูจน์ตรง ๆ: การ assign UniqueKey() ใหม่ตอน rebuild จะประทับ State สดใหม่ (id instance ใหม่) ในขณะที่ rebuild ธรรมดาที่ไม่เปลี่ยน key จะใช้ State เดิมซ้ำ มีประโยชน์สำหรับ “รีเซ็ต form/field นี้ให้ว่างเปล่า” โดยไม่ต้องมี method reset เฉพาะ | Keys |
use_build_context_synchronously | lint จริงของ analyzer ที่ทำงานเมื่อ context (หรือสิ่งที่มาจากมัน) ถูกใช้หลัง await ข้างใน event handler โดยไม่มีการ์ด mounted คั่นกลาง — widget อาจถูก unmount ไปแล้วระหว่างที่งานที่ await ค้างอยู่ ทำให้การใช้ context ทีหลังนั้นไม่ปลอดภัย | Build & BuildContext |
| Term | ความหมาย | บทเรียน |
|---|---|---|
ValueKey | ระบุตัวตน element ด้วยค่าข้อมูล (id, string) แทนที่จะเป็นตำแหน่ง — ทางแก้มาตรฐานของบั๊ก state ค้างเมื่อสลับลำดับ: พิสูจน์ตรง ๆ ว่า ValueKey(label) ทำให้สถานะ checked ของ checkbox ตามป้ายชื่อของมันไปแม้ list จะถูกสลับ ในขณะที่ไม่มี key จะปล่อยให้มันค้างอยู่ที่ตำแหน่งเดิม | Keys |
ValueNotifier / ValueListenableBuilder | ทางเลือกที่แคบกว่าเมื่อเทียบกับการ “lift state up” แบบ setState ไร้เดียงสา: ValueNotifier<T> ถือค่าไว้นอกเส้นทาง rebuild ของ widget แล้วมีแค่ builder ของ ValueListenableBuilder เท่านั้นที่ rebuild เมื่อมันเปลี่ยน — พิสูจน์ตรง ๆ ด้วย build-counter test: setState แบบ lift ธรรมดาทำให้ build ของ sibling ที่ไม่ได้ฟังรันซ้ำทุกครั้ง (1→4 ตลอดสามการอัปเดต) ในขณะที่ sibling ของ ValueListenableBuilder ที่เทียบเท่ากันไม่ขยับจาก 1 เลย | Stateless vs. Stateful |
| Term | ความหมาย | บทเรียน |
|---|---|---|
WillPopScope (ถูกเอาออก) | วิธีสกัดกั้น back gesture ก่อนยุค PopScope — deprecated ตาม doc-comment annotation ที่ quote ตรง ๆ จาก source ของ SDK 3.47.5 ที่ติดตั้งจริง PopScope (ด้านบน) คือตัวแทนจริงในปัจจุบัน ที่ยังจำเป็นเพิ่มเติมสำหรับความเข้ากันได้กับ predictive-back gesture | Navigator & Routes |
| Widget tree | ค่า config ที่ไม่เปลี่ยนแปลงที่เราเขียนในทุก method build — rebuild ตลอดเวลา ทิ้งได้ ถูก createElement() คือ method เดียวที่ widget ทุกตัวมีที่สร้างสิ่งที่คงอยู่จริง (Element) การประกอบกัน ไม่ใช่การสืบทอด: เราห่อ widget ด้วย Padding/Center ไม่เคย subclass มันเพื่อเพิ่มระยะห่าง — widget tree คือ stylesheet เอง ไม่มีภาษา layout แยกต่างหาก | Widgets and the Tree |
| Term | ความหมาย | บทเรียน |
|---|---|---|
--obfuscate --split-debug-info | flutter build apk --release --obfuscate --split-debug-info=<dir> จริง ทำให้ identifier ใน binary ที่ส่งออกถูก obfuscate และเขียน symbol mapping จริงที่ไม่ obfuscate ไปไว้ที่ <dir> แยกต่างหาก จำเป็นสำหรับ symbolize stack trace ที่ obfuscate ของ crash จริงในภายหลัง — พิสูจน์ด้วย build Android จริงบน Mac เครื่องนี้; คำเตือนจริงที่โผล่ออกมาใน output นั้นระบุว่า ELF library ที่สร้างขึ้นยังมี DWARF debugging info ที่ไม่ obfuscate อยู่ เว้นแต่จะส่ง --strip ไปด้วย พิสูจน์แล้วว่า --obfuscate เพียว ๆ โดยไม่เก็บ output ของ --split-debug-info ที่ตรงกันไว้ ไม่พอสำหรับ crash report ที่อ่านได้ในภายหลัง | Build & Release |
--wasm (flutter build web --wasm) | คอมไพล์เป็น WebAssembly แทน JS ของ dart2js — ตัวเลขจริงที่ทำซ้ำได้จากแอป counter default ของคอร์สนี้: main.dart.js (dart2js) ~1.8MB เทียบกับ output ของ wasm (main.dart.wasm + main.dart.mjs) รวม ~1.4MB เล็กกว่าจริงสำหรับแอปนี้ (ไม่ใช่กฎทั่วไป) ข้อจำกัดจริงเรื่อง browser quote จาก docs.flutter.dev: การรองรับที่เชื่อถือได้ต้องการ WasmGC ซึ่ง ณ SDK ของคอร์สนี้หมายถึง Chromium/Chrome/Edge 119+ เท่านั้น — ทั้ง Safari และ Firefox ต่างมีบั๊กความเข้ากันได้ที่ยังเปิดอยู่แม้จะประกาศรองรับแล้ว และ Chrome บน iOS (ใช้ WebKit) ไม่รองรับ WasmGC เลย ยังต้องการ response header Cross-Origin-Embedder-Policy/Cross-Origin-Opener-Policy ที่เจาะจงจากฝั่งที่ serve build ด้วย | Build & Release |