ข้ามไปยังเนื้อหา

อภิธานศัพท์

ศัพท์ของ 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 / FadeTransitionwidget animation แบบ implicit ที่ห่อกลไก RenderOpacity/layer เดียวกับที่พิสูจน์แล้วสำหรับ widget Opacity ธรรมดา — มันเพิ่มแค่การจัดตารางด้วย ticker กับการ dispose อัตโนมัติ ไม่ได้ ทำให้ไม่ต้องจ่ายค่า compositing layer จริงในทุกเฟรมที่ alpha ไม่เท่ากับศูนย์ระหว่าง fadeAnimations
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 / awaitasync ทำให้ function คืนค่าเป็น Future<T>; await แกะมันออกมาแล้วปล่อยให้ error ปรากฏเป็น exception ปกติที่ try/catch ได้ — พิสูจน์ตรง ๆ กับ Future ที่ reject แล้วจริง แทบไม่ await ใน build เลย แต่ส่ง Future ให้ FutureBuilder แทนAsync in Dart
AsyncNotifier / AsyncValue.whenprovider สำหรับ 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 เป็นชนิดจริงผ่านการสืบทอดจริง ไม่ใช่ของปลอมที่แค่สร้างหน้าตาให้ตรง interfaceClasses & Mixins
BLoC / Blocpattern ที่บังคับให้ 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 / buildWhenBlocBuilder<B, S> rebuild ทุกครั้งที่มี state ใหม่โดย default; buildWhen: (previous, current) => ... ใช้จำกัดขอบเขตนั้น — พิสูจน์ด้วยตัวนับ build: สี่ event Increment ที่ runtimeType ของ state ไม่เปลี่ยนทำให้ build เป็นศูนย์ครั้ง แล้ว event ที่ห้า (ที่ type เปลี่ยนจริง) ทำให้ build หนึ่งครั้ง บทบาทเดียวกับ aspect ของ InheritedModel หรือ .select() ของ RiverpodBLoC & Patterns
BlocListenerรัน side effect ครั้งเดียว (การนำทาง, SnackBar) ตอบสนองต่อ state ใหม่ โดยไม่ rebuild widget ไหนเลย — พิสูจน์แล้วว่ายิงพอดีครั้งเดียว ด้วยค่า state ที่ trigger จริง ตลอดห้า eventBLoC & Patterns
blocTesthelper ของ bloc_test (10.0.0): build bloc ขึ้นมา ขับด้วย act แล้ว assert ลำดับ state ที่ emit ออกมาแบบเป๊ะ ด้วย expect: () => [...] — ไม่มี widget เกี่ยวข้องเลย seed: ใช้เริ่มจาก state ที่กำหนดโดยไม่ต้อง replay event ที่ปกติต้องทำถึงจะไปถึง state นั้น ต้องให้ class ของ state extend Equatable (ดูด้านล่าง) ไม่งั้น state ที่ค่าเท่ากันจะเทียบไม่เท่ากันแล้ว assertion ทุกอันจะ failBLoC & Patterns
BuildContextไม่ใช่ตัวแปร global ลอย ๆ — มัน คือ Element ณ ตำแหน่งนั้นในต้นไม้ (พิสูจน์ด้วยการ spy createElement) identity นี้เองที่ทำให้ .of(context) ค้นหาขึ้นไปข้างบนหา ancestor ได้ การใช้ context ที่อยู่เหนือ ancestor ที่กำลังหา (เช่น Scaffold.of(context) ใน build เดียวกับที่สร้าง Scaffold) จะโยน error จริงชื่อชัดเจน — Builder คือทางแก้มาตรฐาน เพราะสร้าง context ที่อยู่ใต้จริง ๆBuild & BuildContext
build / rebuild / dirtysetState (หรือ 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>() คือการอ่านแบบไม่ผูก dependencyInheritedWidget
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.antiAliasWithSaveLayerenum พฤติกรรม 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 canonicalizationexpression 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
ConnectionStateenum สี่ค่าที่ 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 / OverflowBoxConstrainedBox เพิ่มขอบ 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.pushcontext.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 registryProject & 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 analyzeNull 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 typesetState & Lifecycle
disposeถูกเรียกเมื่อ Flutter ยืนยันแล้วว่า element ไม่ถูกใส่กลับเข้าที่อื่นหลัง deactivate — จุดสำหรับ cleanup controller, ยกเลิก subscription ลืมเรียกคือ memory leak คลาสสิกของ Flutter พิสูจน์เป็นรูปธรรมกับ Ticker ของ AnimationController ที่ leaksetState & 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 treeinstance ที่มีชีวิต ของ widget ณ ตำแหน่งหนึ่งในต้นไม้ — อยู่ต่อเนื่องข้าม rebuild ถือ State ตัดสินใจว่า widget ใหม่จะอัปเดตมันในที่ (update/didUpdateWidget) หรือต้องรื้อทิ้ง (unmount + createElement ใหม่) BuildContext คือ element (พิสูจน์ด้วยการ spy identity ของมัน) อยู่ตรงกลางของสามต้นไม้ ระหว่าง widget tree ที่ถูกและ render tree ที่แพงWidgets and the Tree
Equatablepackage:equatable (3.0.0) ให้ class มี value-equality (==/hashCode) จากรายการ props ที่ประกาศไว้ แทนที่ identity equality ปกติของ Dart — จำเป็นสำหรับ expect: () => [...] ของ blocTest ที่จะเทียบ state ที่ emit ออกมาแบบ by value; ถ้าไม่มี state สองตัวที่หน้าตาเท่ากันแต่สร้างแยกกันจะเทียบไม่เท่ากัน แล้ว assertion ทุกอันจะ failBLoC & 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 เดิมยังคงเป็น 50pxCore 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 pluginplugin หลักของ 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 / schemesflavor ของ 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_analysisflutter 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.2Project & 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 ที่ต่ำกว่า 30msAsync 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 testtest แบบ snapshot ที่เทียบพิกเซลที่ render กับไฟล์ .png อ้างอิงที่ commit ไว้ผ่าน matchesGoldenFile(...) — ทรงพลังสำหรับ design system แต่เปราะบางข้าม platform/เวอร์ชัน font; regenerate อย่างตั้งใจด้วย flutter test --update-goldens harness ของคอร์สนี้แนบ binary asset อ้างอิงผ่านระบบ fence ไม่ได้ จึงพิสูจน์อย่างตรงไปตรงมาใน scratch แทน ไม่ใช่ shared probeTesting
go_routerpackage การนำทางแบบ 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 ที่วางกลับมาสร้างหน้าจอเดิมได้บน webDeclarative Routing
go_router_builderสร้าง GoRouteData subclass ที่มี type ชัดเจนต่อ route จาก annotation @TypedGoRoute (เวอร์ชันปัจจุบัน: 4.5.0) — เปลี่ยน state.pathParameters['id']! ที่พิมพ์มือและ key เป็น string ให้กลายเป็น field ของ constructor จริงที่เช็คตอน compile-timeDeclarative 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 restartHot reload (r) ฉีดโค้ดที่เปลี่ยนเข้าไปในแอปที่กำลังรันแล้ว rebuild widget tree — State ถูกรักษาไว้ เร็วกว่าวินาที Hot restart (R) ทิ้ง state ทั้งหมดแล้วเริ่มใหม่จาก main() Hot reload รัน build ใหม่ แต่ไม่รัน initState หรือ main ใหม่ และใช้กับการเปลี่ยนค่า const ใหม่หรือเปลี่ยนรูปร่าง class/enum ไม่ได้ — ถ้าเปลี่ยนแล้วดูเหมือนไม่มีผล คำตอบมักเป็น hot restartProject & Tooling
Termความหมายบทเรียน
Impellerrenderer ของ 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.47Render Pipeline
InheritedModelrebuild 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 คืน trueInheritedWidget
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.spawnIsolate.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 เทียบกับ AOTJIT (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 buildBuild & 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, PageStorageKeyKeys
kIsWebbool ที่รู้ตอน compile-time เป็น true เฉพาะบน web target — ตัวป้องกันจริงสำหรับแยกโค้ดที่แตะ dart:io ออกจาก web build เพราะ dart:io เอง compile ผ่าน (เป็น stub) แต่โยน UnsupportedError ตอน runtime บน web แทนที่จะ build ไม่ผ่านProject & Tooling
kReleaseModeflag 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-l10npipeline 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.routerconstructor variant ของ MaterialApp สำหรับการนำทางแบบ declarative (Navigator 2.0) — รับ routerConfig: (instance ของ GoRouter ในคอร์สนี้) แทน home:/routes: ให้ router (ไม่ใช่ MaterialApp เอง) เป็นเจ้าของ route stack ทั้งหมดApp Scaffolding
MediaQuery / SafeAreaMediaQuery.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 จริง ไม่ใช่แค่ stubPlatform Channels
mixin classclass ที่ใช้ได้ทั้งเป็น class Foo และ with Foo (mixin) — ประกาศ generative constructor ที่ไม่ trivial ไม่ได้ เพราะ field ของ mixin จะถูกต่อกิ่งเข้ากับ class ไหนก็ตามที่ apply มัน ซึ่ง constructor จะไม่มีความหมาย error จริง: mixin_class_declares_non_trivial_generative_constructorClasses & 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
mountedgetter จริงที่ไม่เป็น 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/desktopApp Scaffolding
Navigator / route stackAPI แบบ 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 ให้ (ดูหัวข้อนั้น) โดยไม่ต้องมี defaultDart 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 / MissingPluginExceptionPlatformException คือ 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>() อ่านมันแบบมี (หรือไม่มี) การผูก rebuildProvider & Riverpod
ProviderScopewidget ที่สร้างและเปิดเผย ProviderContainer ของ Riverpod — ไม่ใช่ตัวเลือก: ทุก ref.watch/ref.read ต้องการมันอยู่ข้างบนที่ไหนสักแห่ง ข้ามมันไปจะโยน error จริง StateError: "Bad state: No ProviderScope found" ทันทีที่ ConsumerWidget พยายามอ่าน provider วางครั้งเดียวที่ root ของแอป ห่อ runApp() ไว้Provider & Riverpod
pubspec.yamlmanifest ของโปรเจกต์: 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/.$2Dart Language Tour
redirect / refreshListenableredirect รันก่อน 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 ที่นิ่งจะค้างที่ 1Render 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
scrollCacheExtentAPI ปัจจุบันสำหรับควบคุมว่า 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 ไป 26Slivers & 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 ใน BLoCClasses & Mixins
.select()การอ่าน provider ของ Riverpod ที่ rebuild เฉพาะตอนที่ค่าที่ derive มาเจาะจงเปลี่ยน ไม่ใช่ state ของ provider ทั้งก้อน — เทียบเท่ากับ aspect ของ InheritedModel หรือ buildWhen ของ BLoC ใน RiverpodProvider & 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 / SliverAppBarsliver คือพื้นที่ 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 / StateStatefulWidget ยังคง 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
StatelessWidgetUI ที่เป็น pure function ของ input จาก constructor เท่านั้น — ไม่มีข้อมูลที่เปลี่ยนแปลงได้เป็นของตัวเอง มีแค่ build เท่านั้น ใช้กับสิ่งที่ไม่เปลี่ยนแปลงเองตามเวลาStateless vs. Stateful
Termความหมายบทเรียน
testWidgets variantvariant: someTestVariant รัน test body เดิม หนึ่งครั้งต่อค่าใน TestVariant — พิสูจน์ด้วย ValueVariant<String>({'en','th','ja'}) จริง: เรียก testWidgets ครั้งเดียว ได้ผลลัพธ์การรัน test แยกกันสามครั้ง TargetPlatformVariant.all() คือ variant สำเร็จรูปสำหรับรัน widget test เดียวข้ามทุก TargetPlatform โดยไม่ต้องเขียน loop เองTesting
Ticker / TickerProviderTicker ยิง 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_futureslint ที่จับการเรียก 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_synchronouslylint จริงของ 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 gestureNavigator & 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-infoflutter 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