Production Concerns
จับ error ทุกตัว — และเข้าใจกลไกปัจจุบันให้ถูก
หัวข้อที่มีชื่อว่า “จับ error ทุกตัว — และเข้าใจกลไกปัจจุบันให้ถูก”ใน production คุณต้องการรู้เรื่อง crash ก่อนที่ผู้ใช้จะบ่น นี่คือหนึ่งในจุดที่คำแนะนำอย่างเป็นทางการของ Flutter เปลี่ยนไปจริง ๆ และการเข้าใจคำแนะนำปัจจุบันให้ถูก (ไม่ใช่ตัวที่เคยเป็นเมื่อสองสามปีก่อน) สำคัญ
FlutterError.onError: framework error พิสูจน์แล้ว
หัวข้อที่มีชื่อว่า “FlutterError.onError: framework error พิสูจน์แล้ว”import 'package:flutter/material.dart';
class ThrowsInBuild extends StatelessWidget { const ThrowsInBuild({super.key}); @override Widget build(BuildContext context) { throw StateError('build error'); }}import 'dart:ui';import 'package:flutter/material.dart';import 'package:flutter_test/flutter_test.dart';import 'package:probe/lessons/tooling-and-production__production-concerns/error_demo.dart';
void main() { testWidgets( 'FlutterError.onError catches a build() error; PlatformDispatcher.onError does not', (tester) async { FlutterErrorDetails? caughtByFlutterError; Object? caughtByPlatformDispatcher;
final originalFlutterOnError = FlutterError.onError; final originalPlatformOnError = PlatformDispatcher.instance.onError;
FlutterError.onError = (details) => caughtByFlutterError = details; PlatformDispatcher.instance.onError = (error, stack) { caughtByPlatformDispatcher = error; return true; };
await tester.pumpWidget(const MaterialApp(home: ThrowsInBuild()));
FlutterError.onError = originalFlutterOnError; PlatformDispatcher.instance.onError = originalPlatformOnError;
expect(caughtByFlutterError?.exception, isA<StateError>()); expect(caughtByPlatformDispatcher, isNull); // genuinely NOT forwarded here expect(tester.takeException(), isNull); // FlutterError.onError handled it, nothing escaped });}00:00 +0: FlutterError.onError catches a build() error; PlatformDispatcher.onError does not
00:00 +1: All tests passed!พิสูจน์แล้วตรง ๆ: error จาก build/layout/paint ไปที่ FlutterError.onError และอยู่ที่นั่น — PlatformDispatcher.instance.onError ไม่เห็นมันเลย และ tester.takeException() คืน null เพราะ FlutterError.onError ดูดมันไว้เต็มที่ (ความต่างแบบ “ถูกดูดไว้ vs หลุดออกมา” เดียวกับที่พิสูจน์แล้วสำหรับ FutureBuilder ในบทเรียน futures-streams-in-ui)
PlatformDispatcher.instance.onError: คำตอบปัจจุบันสำหรับทุกอย่างที่เหลือ
หัวข้อที่มีชื่อว่า “PlatformDispatcher.instance.onError: คำตอบปัจจุบันสำหรับทุกอย่างที่เหลือ”หลายปีที่ผ่านมา คำแนะนำมาตรฐาน (และบทเรียนนี้เวอร์ชันก่อนหน้าเอง) คือการจับคู่ FlutterError.onError กับ runZonedGuarded ที่เขียนเองรอบ runApp() เพื่อจับ async error คำแนะนำนั้นล้าสมัยแล้ว ตรง ๆ จาก release notes 3.3 ของ Flutter เอง อ้างอิงตรง ๆ: “In previous releases, you had to manually configure a custom Zone in order to catch all of an application’s exceptions and errors. However, custom Zones were detrimental to a number of optimizations in Dart’s core libraries, which slowed down application start-up time. In this release, instead of using a custom Zone, you should catch all errors and exceptions by setting the PlatformDispatcher.onError callback.” คู่มือ error-handling ของ docs.flutter.dev ยืนยันช่องว่างเจาะจงที่ PlatformDispatcher.instance.onError ปิด: “Consider an onPressed callback that invokes an asynchronous function… If invokeMethod throws an error, it won’t be forwarded to FlutterError.onError. Instead, it’s forwarded to the PlatformDispatcher.”
void main() { FlutterError.onError = (details) { FlutterError.presentError(details); reportToCrashService(details.exception, details.stack); };
PlatformDispatcher.instance.onError = (error, stack) { reportToCrashService(error, stack); // the current, official replacement for runZonedGuarded return true; // tells the engine the error was handled };
runApp(const MyApp());}ช่องว่างที่ตรงไปตรงมาและทดสอบแล้วใน environment ของคอร์สนี้เอง: การพิสูจน์ว่า PlatformDispatcher.instance.onError ทำงานจริงสำหรับ async error ที่ไม่ถูกจับจริง ๆ (เช่น Future ที่ไม่ await แล้ว throw) ทำซ้ำไม่ได้ภายใต้ flutter test — ความพยายามรันจริงที่นี่แสดงว่า test zone ของ package:test เอง (และภายใต้ testWidgets คือ binding ที่ใช้ FakeAsync) ดัก async error ที่ไม่ถูกจับไว้เพื่อรายงาน test failure ของตัวเองก่อนที่มันจะไปถึง PlatformDispatcher.instance.onError ที่กำหนดเองเลยด้วยซ้ำ ทั้งใน test() ธรรมดาและ testWidgets() นี่คือข้อจำกัดของ tooling ใน test environment จริง ๆ ไม่ใช่ข้อกล่าวอ้างเกี่ยวกับพฤติกรรมของแอปจริง — docs ทางการที่อ้างอิงด้านบนชัดเจนว่า PlatformDispatcher.instance.onError คือสิ่งที่แอปจริงที่กำลังรันอยู่ (นอก test harness ใด ๆ) ได้รับสำหรับ error class นี้ runZonedGuarded เองยังคงทำงานตามที่พิสูจน์แล้วในบทเรียน Async in Dartเป๊ะ — มันไม่ได้ deprecated หรือพัง — มันแค่ไม่ใช่กลไกที่แนะนำสำหรับงานนี้เจาะจงอีกต่อไป ด้วยเหตุผลด้าน startup performance ที่อ้างอิงไว้ด้านบน
Crash reporting และ logging
หัวข้อที่มีชื่อว่า “Crash reporting และ logging”ต่อ handler ทั้งสองด้านบนเข้ากับ service — Sentry, Firebase Crashlytics — เพื่อให้ crash จากอุปกรณ์จริงกลับมาพร้อม stack trace, ข้อมูลอุปกรณ์ และ breadcrumb:
// Code shape (Sentry), cited from sentry.io's Flutter docs — not// independently run here, no external network/service in this environment.PlatformDispatcher.instance.onError = (error, stack) { Sentry.captureException(error, stackTrace: stack); return true;};สองกฎที่สำคัญไม่ว่าจะใช้ provider ไหน:
- Upload symbol/debug info สำหรับ release build — AOT ตัด symbol ออก และถ้าใช้
--obfuscateidentifier ก็ถูกเปลี่ยนชื่อด้วย; ถ้าไม่มีไฟล์ symbol ที่ตรงกันเป๊ะ stack trace จะอ่านไม่ออก (พิสูจน์เต็มรูปแบบ พร้อม--split-debug-infobuild จริงและไฟล์ symbol จริง ในบทเรียน build-and-release) - Log อย่างตั้งใจ และรู้ว่าอะไรรอดจริง ๆ ใน release — บทเรียนนี้เวอร์ชันก่อนหน้าอ้างว่า
print“stripped/ignored in release” การอ่าน source ของ SDK ที่ติดตั้งจริงตรง ๆ (~/flutter/packages/flutter/lib/src/foundation/print.dart) แก้ข้อนั้น: ทั้งprintและdebugPrintไม่ถูกตัดใน release เลย doc comment ของdebugPrintเองระบุตรง ๆ: “ThedebugPrintfunction logs to console even in release mode. As per convention, calls todebugPrintshould be within a debug mode check or an assert.” เหตุผลจริงที่ควรใช้debugPrintแทนprintเปล่า ๆ ไม่ใช่เรื่องการตัดทิ้งใน release — แต่คือการปิดเสียง (throttling): implementation default ของdebugPrintคือdebugPrintThrottledเจาะจงเพราะ ตาม doc comment ของมันเอง การพิมพ์เร็ว ๆ โดยไม่ปิดเสียงอาจทำให้เกิดข้อมูลหายบน Android (buffer log ของ platform ทิ้งบรรทัดภายใต้ burst ปริมาณสูง) —debugPrintSynchronouslyมี signature เดียวกันแต่ไม่ปิดเสียง สำหรับตอนที่ลำดับการสลับกันสำคัญกว่าการหลีกเลี่ยงข้อมูลหาย ฟังก์ชันlog()ของdart:developerคือตัวเลือกจริงอีกตัวที่ต่างจากทั้งสอง: มันเขียน log record แบบมีโครงสร้าง (มีname,level,error,stackTrace) ที่มุมมอง logging ของ DevTools เองกรองได้ แทนที่จะเป็น string แบนราบ
การตัด assert พิสูจน์ด้วยการ compile จริง
หัวข้อที่มีชื่อว่า “การตัด assert พิสูจน์ด้วยการ compile จริง”assert(...) (และ block ที่ถูก gate ด้วย kDebugMode ใด ๆ เพราะ kDebugMode เองถูกนิยามด้วยกลไกเดียวกัน) ไม่ทำงานใน release build — และเหตุผลจริงพิสูจน์ได้ตรง ๆ: การรัน assert ถูก gate ด้วย flag --enable-asserts ของ Dart VM/compiler ซึ่ง tooling ของ Flutter เองส่งให้อัตโนมัติสำหรับการรัน debug (flutter run, flutter test) และไม่เคยส่งให้ flutter build ... --release environment ของคอร์สนี้รัน output ของ flutter build web --release ที่ compile แล้วไม่ได้ (ไม่มี browser) ดังนั้นกลไกเดียวกันเป๊ะจึงพิสูจน์ตรง ๆ ด้วย dart compile exe ธรรมดา — backend AOT ของ Dart ตัวเดียวกับที่ flutter build ใช้ข้างใต้ ควบคุมด้วย flag ตัวเดียวกันเป๊ะ:
// assert_check.dart -- plain Dart, run in scratch (not the shared probe)void main() { var assertsRan = false; assert(() { assertsRan = true; return true; }()); print(assertsRan ? 'ASSERTS RAN (enabled)' : 'ASSERTS DID NOT RUN (stripped)');}$ dart run assert_check.dart # JIT, no flagASSERTS DID NOT RUN (stripped)
$ dart run --enable-asserts assert_check.dart # JIT, matches `flutter run`/`flutter test` debug modeASSERTS RAN (enabled)
$ dart compile exe assert_check.dart -o release_build # AOT, no flag -- matches `flutter build ... --release`$ ./release_buildASSERTS DID NOT RUN (stripped)
$ dart compile exe --enable-asserts assert_check.dart -o release_build_forced # AOT, flag forced on$ ./release_build_forcedASSERTS RAN (enabled)การรันครั้งที่สี่คือตัวสำคัญ: binary ที่ compile แบบ AOT รัน assert ได้จริง ๆ ถ้า --enable-asserts ถูกส่งให้ — พิสูจน์ว่า assert ถูก gate ด้วย flag ตัวเดียวนี้ ไม่ใช่อะไรที่เป็นไปไม่ได้เชิงโครงสร้างใน AOT/release code flutter build ... --release แค่ไม่เคยส่ง flag นั้นเท่านั้นเอง ซึ่งคือกลไกทั้งหมดที่พิสูจน์ได้เบื้องหลัง “assert ไม่ทำงานใน release” ไม่ใช่ pass การตัดทิ้งแยกต่างหากพิเศษ
Accessibility พื้นฐาน: Semantics กับ meetsGuideline พิสูจน์แล้ว
หัวข้อที่มีชื่อว่า “Accessibility พื้นฐาน: Semantics กับ meetsGuideline พิสูจน์แล้ว”import 'package:flutter/material.dart';
class UnlabeledIconButton extends StatelessWidget { const UnlabeledIconButton({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: IconButton( icon: const Icon(Icons.delete), onPressed: () {}, // no tooltip / no Semantics label ), ); }}
class LabeledIconButton extends StatelessWidget { const LabeledIconButton({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: Semantics( label: 'Delete item', child: IconButton( icon: const Icon(Icons.delete), onPressed: () {}, tooltip: 'Delete item', ), ), ); }}import 'package:flutter/material.dart';import 'package:flutter_test/flutter_test.dart';import 'package:probe/lessons/tooling-and-production__production-concerns/a11y_demo.dart';
void main() { testWidgets('a labeled icon button meets the labeled-tap-target guideline', (tester) async { final handle = tester.ensureSemantics(); await tester.pumpWidget(const MaterialApp(home: LabeledIconButton())); await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); handle.dispose(); });
testWidgets('an unlabeled icon button FAILS the labeled-tap-target guideline', (tester) async { final handle = tester.ensureSemantics(); await tester.pumpWidget(const MaterialApp(home: UnlabeledIconButton())); await expectLater( tester, doesNotMeetGuideline(labeledTapTargetGuideline), ); handle.dispose(); });}00:00 +0: a labeled icon button meets the labeled-tap-target guideline00:00 +1: an unlabeled icon button FAILS the labeled-tap-target guideline
00:00 +2: All tests passed!ข้อความ failure จริงที่จับได้บนปุ่มที่ไม่มี label (จากกรณีลบ รันตรงกับ meetsGuideline ก่อนที่จะพลิกเป็นรูปแบบ doesNotMeetGuideline ที่ผ่านซึ่งใช้ใน fence ด้านบน):
Expected: Tappable widgets should have a semantic label Actual: <Instance of 'WidgetTester'> Which: SemanticsNode#4(Rect.fromLTRB(0.0, 0.0, 48.0, 48.0), actions: [focus, tap], flags: [isButton, hasEnabledState, isEnabled, isFocusable]): expected tappable node to have semantic label, but none was found.meetsGuideline/doesNotMeetGuideline รับ AccessibilityGuideline ที่มีมาให้ของ Flutter ตัวใดก็ได้ (labeledTapTargetGuideline, textContrastGuideline, androidTapTargetGuideline, iOSTapTargetGuideline) แล้วรันการเช็คเชิงกลไกจริงกับ semantics tree ที่ render แล้ว — tester.ensureSemantics() ต้องเรียกก่อน (มันสร้าง semantics tree ที่ test binding ข้ามไปเพื่อ performance ปกติ เพราะ widget test ส่วนใหญ่ไม่ต้องการมัน) และต้อง .dispose() หลังจากนั้น
Performance overlay (docs — ไม่มีอุปกรณ์ที่นี่)
หัวข้อที่มีชื่อว่า “Performance overlay (docs — ไม่มีอุปกรณ์ที่นี่)”MaterialApp(showPerformanceOverlay: true) วาดกราฟ real-time สองอันทับบนแอปที่กำลังรันตรง ๆ: เวลาต่อเฟรมของ UI thread (build/layout/paint) และเวลาต่อเฟรมของ raster thread แต่ละแท่งแสดงสีแดงเมื่อเฟรมเกินงบ มันคือวิธีที่เร็วที่สุดในการเห็นจังหวะสะดุดขณะโต้ตอบกับแอปสด ๆ เสริมกับ frame timeline ของ DevTools เอง (ครอบคลุมพร้อม workflow จริงในบทเรียน rebuilds-and-performance) environment ของคอร์สนี้ไม่มีอุปกรณ์/emulator ให้ render และ screenshot overlay จริง ๆ ดังนั้นนี่คือการอ้างอิงจากคู่มือ performance overlay ของ docs.flutter.dev แทนที่จะสาธิต
Environment config
หัวข้อที่มีชื่อว่า “Environment config”กลไก --dart-define/--dart-define-from-file เดียวกับที่พิสูจน์แล้วในบทเรียน build-and-release (test จริงที่นั่นยืนยันว่าค่าที่กำหนดถูกอ่านเป็น compile-time constant จริง ๆ) คือวิธีมาตรฐานในการ inject config ต่อ environment (API base URL, feature flag) โดยไม่ต้อง commit secret ลง source หรือ branch โค้ดต่อ environment — รวมกับ flavor สำหรับอะไรก็ตามที่ต้องการ identity ของแอปที่ต่างกันจริง ๆ (bundle ID, ไอคอน) ไม่ใช่แค่ค่า config ที่ต่างกัน
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”การจัดการ error สำหรับ production ที่ AI generate มักหยิบ pattern runZonedGuarded แบบเก่า (มันคือสิ่งที่ training data และ tutorial ส่วนใหญ่ยังแสดงอยู่) และพลาดความต่างที่บทนี้พิสูจน์ว่าแต่ละ handler ได้รับอะไรจริง ๆ
// AI-generated: "set up crash reporting"void main() { runZonedGuarded(() { runApp(const MyApp()); }, (error, stack) { reportToCrashService(error, stack); });}ก่อนจะเชื่อ setup การจัดการ error ที่ generate มาแบบนี้ ให้เช็ค:
- ไม่มี
FlutterError.onErrorเลย — พิสูจน์แล้วด้านบนว่า zone ของrunZonedGuardedไม่ได้รับ error จาก build/layout/paint ของ framework; สิ่งเหล่านั้นต้องการ handler ของตัวเอง หรือไม่งั้นก็แสดงแค่หน้าจอ error สีแดง default เท่านั้น runZonedGuardedที่ใช้เป็นกลไกเดียวสำหรับ async error โดยไม่มีPlatformDispatcher.instance.onError— พิสูจน์แล้วด้านบนจาก docs ทางการว่านี่คือ pattern ที่ล้าสมัย; ช่องว่างเจาะจงที่มันทิ้งไว้ (async error จาก plugin/MethodChannelที่ไม่ถูกส่งไปยังFlutterError.onError) คือสิ่งที่PlatformDispatcher.instance.onErrorมีเอกสารระบุว่าปิดพอดี- statement
print/debugPrintที่ทิ้งไว้เพื่อ “production logging” โดยสมมติว่ามันหายไปใน release — พิสูจน์แล้วด้านบนจาก source ว่าไม่จริง อะไรที่ sensitive ที่ log แบบนี้ไปถึง device log ของผู้ใช้จริง assertที่ใช้ guard อะไรบางอย่างที่ต้องเกิดขึ้นจริงใน production (validation จริง ไม่ใช่แค่ sanity check สำหรับ debug) — พิสูจน์แล้วด้านบนว่า assert ถูก gate ด้วย compile flag ที่ release build ของ Flutter ไม่เคยส่ง โค้ดข้างในมันไม่ทำงานเลยสำหรับผู้ใช้จริง
🐛 หา bug ในโค้ดนี้
void main() {runZonedGuarded(() { FlutterError.onError = (details) { reportToCrashService(details.exception, details.stack); }; runApp(const MyApp());}, (error, stack) { reportToCrashService(error, stack);});}
class ProfileScreen extends StatelessWidget {const ProfileScreen({super.key, required this.userId});final String userId;
@overrideWidget build(BuildContext context) { fetchProfile(userId); // fire-and-forget, not awaited return const CircularProgressIndicator();}}เฉลย
มีปัญหาสองอย่างที่ซ้อนกัน ทั้งคู่พิสูจน์แล้วด้านบน อย่างแรก setup นี้ใช้ pattern แบบเก่าที่พึ่ง runZonedGuarded อย่างเดียวสำหรับ async error แทนที่จะเป็น PlatformDispatcher.instance.onError ที่มีเอกสารปัจจุบัน — ช่องว่างจริงที่ docs ทางการระบุตรง ๆ: async error จาก plugin call ถูก “forwarded to the PlatformDispatcher” ไม่ถูกจับโดย zone นี้หรือโดย FlutterError.onError อย่างที่สอง fetchProfile(userId) ข้างใน build คือการเรียกแบบ fire-and-forget (บทเรียน Async in Dart พิสูจน์ว่า pattern นี้เป๊ะ ๆ ทำให้ error ไม่ถูกจับโดย try/catch ใด ๆ ที่ call site จริง ๆ) — ดังนั้นถ้ามัน throw setup ทั้งหมดนี้พนันทุกอย่างไว้กับ handler runZonedGuarded ตัวเดียวที่ถูกแทนที่ไปแล้วให้จับมัน โดยไม่มี PlatformDispatcher.instance.onError เป็น backstop สำหรับ async path ที่ zone ของมันเองอาจไปไม่ถึง
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกกลไกในบทนี้ตอบคำถามเดียวกับเนื้อหา error-handling ก่อนหน้าในคอร์ส ที่ชั้น production: เมื่อ error เกิดขึ้น มีอะไรข้างล่างรู้เรื่องไหม และความจริงรอดไปถึงที่นั่นมากแค่ไหน? FlutterError.onError เทียบกับ PlatformDispatcher.instance.onError คือ “error มาจากชั้นไหน” (build ของ framework เทียบกับทุกอย่างที่เหลือ) — พิสูจน์แล้วด้านบนว่าเป็นการแบ่งที่แยกจากกันจริง ไม่ใช่ coverage ที่ซ้อนกัน assert/debugPrint ทั้งคู่เกี่ยวกับ “อะไรรอดจากการเดินทางจาก editor ของคุณไปถึงอุปกรณ์ของผู้ใช้จริง” — พิสูจน์แล้วด้านบนว่าตัวหนึ่งถูกตัดทิ้งเต็มที่ผ่านการไม่ส่ง flag ส่วนอีกตัวไม่ถูกตัดจริง ๆ (แค่ปิดเสียง) และ obfuscation + ไฟล์ symbol (จากบทเรียน build-and-release) คือคำถามเดียวกันที่ชั้น binary: stack trace ที่ crash service ได้รับอ่านได้ดีแค่ไหนขึ้นอยู่กับไฟล์ symbol เป๊ะที่คุณเก็บไว้คู่กับ build นั้นเท่านั้น
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ข้อดี | ต้นทุน |
|---|---|---|
PlatformDispatcher.instance.onError (ปัจจุบัน) | พิสูจน์แล้วด้านบนจาก docs ทางการ: จับสิ่งที่ FlutterError.onError จับไม่ได้ โดยไม่มีต้นทุน startup ของ custom Zone | พิสูจน์แล้วด้านบน: unit test ตรง ๆ ยากจริง ๆ (tooling ของ test ดักไว้ก่อน) — เชื่อ docs ยืนยันใน crash-reporting integration จริง |
runZonedGuarded (legacy แต่ยังทำงานได้) | พิสูจน์แล้วด้านบน (บทเรียนที่ลิงก์ไว้): ยังทำงานได้จริง ให้เหตุผลง่ายในฐานะ zone handler ธรรมดา | พิสูจน์แล้วด้านบนจาก docs ทางการ: ต้นทุน startup performance เป๊ะที่เป็นเหตุผลให้ย้ายออกจากมัน |
debugPrint แทน print | พิสูจน์แล้วด้านบน: ปิดเสียง หลีกเลี่ยงข้อมูลหายจริงบน Android | พิสูจน์แล้วด้านบน: ทั้งคู่ไม่ถูกตัดใน release — ยังต้องการ guard kDebugMode/logger จริงสำหรับอะไรที่ sensitive |
assert สำหรับ invariant ภายใน | ต้นทุนเป็นศูนย์ใน release — พิสูจน์แล้วด้านบนว่า flag ที่รันมันไม่เคยถูกส่งโดย flutter build --release | พิสูจน์แล้วด้านบน: พึ่งพาไม่ได้กับอะไรที่ต้อง execute จริงสำหรับผู้ใช้จริง |
Semantics/meetsGuideline ใน test | พิสูจน์แล้วด้านบน: จับช่องว่าง accessibility จริงที่เจาะจงเชิงกลไก ใน CI โดยไม่มีอุปกรณ์ | ครอบคลุมแค่สิ่งที่ guideline ที่มีมาให้เช็ค — ไม่ใช่ตัวแทนของการ test screen-reader จริง |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
FlutterError.onError+runZonedGuardedยังคงเป็นคู่ที่แนะนำ” พิสูจน์แล้วด้านบนว่าไม่จริง ด้วยการอ้างอิง release notes 3.3 ของ Flutter เองตรง ๆ:PlatformDispatcher.instance.onErrorคือตัวแทนที่มีเอกสารระบุแล้วสำหรับครึ่งของrunZonedGuardedเจาะจงด้วยเหตุผลด้าน startup performance - “
print/debugPrintถูกเอาออกอัตโนมัติใน release build” พิสูจน์แล้วด้านบนว่าไม่จริง จากการอ่าน source ของ SDK ที่ติดตั้งจริงตรง ๆ: doc comment ของdebugPrintเองระบุว่ามัน log “even in release mode” - “
debugPrintมีอยู่หลัก ๆ เพื่อไม่ให้ logging ship ไป production” พิสูจน์แล้วด้านบนว่าไม่จริง: doc comment ของมันเองให้เหตุผลจริง — การปิดเสียง เพื่อหลีกเลี่ยงข้อมูลหายจริงบน log buffer ของ Android ภายใต้ output ปริมาณสูง - “โค้ดข้างใน
assert(...)อาจยังทำงานใน release ถ้า compiler ตัดสินว่ามันถูกพอ” พิสูจน์แล้วด้านบนว่าไม่จริง ด้วยตัวอย่างโต้แย้งที่บังคับจริง: AOT binary ที่ compile ด้วย--enable-assertsรัน assert ของมันจริง ๆ — กลไกคือ flag ที่ release build ของ Flutter ไม่เคยส่ง ไม่ใช่ข้อจำกัดเชิงโครงสร้างของ AOT