Expense Tracker App

แอปจัดการรายจ่ายจากคอร์ส Flutter Academind Section 5-6 — เป็นแอปแรกที่ประกอบ concept หลายตัวเข้าด้วยกัน

Repo: flutter-expense-tracker-app

Widget Tree

graph TD
  MaterialApp --> Scaffold
  Scaffold --> AppBar["AppBar + IconButton"]
  Scaffold --> Column
  Column --> Chart["Chart (FractionallySizedBox)"]
  Column --> ExpensesList["ExpensesList (ListView.builder)"]
  ExpensesList --> Dismissible["Dismissible + ValueKey"]
  Dismissible --> ExpenseItem
  ExpenseItem --> Card
  Card --> Row
  Row --> ColLeft["Column (title + date)"]
  Row --> Spacer
  Row --> RowRight["Row (icon + amount)"]

สิ่งที่เรียนจาก Section 5

UI & Layout

  • AppBar + actions — แถบบน + ปุ่ม IconButton เปิด modal
  • Card + Padding + Row + Spacer — layout ของแต่ละ expense item
  • ListView.builder — สร้าง item เฉพาะที่เห็นบนจอ ประหยัด memory (ต่างจาก Column ที่สร้างทุกตัว)
  • Stack — ซ้อน widget ทับกัน

State Management

  • StatefulWidget — จัดการ _registeredExpenses list
  • setState() — บอก Flutter ให้ rebuild UI เมื่อ list เปลี่ยน

User Input

  • TextEditingController + dispose() — จัดการ input title/amount (ต้อง dispose ไม่งั้น memory leak)
  • DropdownButton — เลือกหมวดหมู่ expense
  • showDatePicker + async/await — เลือกวันที่
  • Validationdouble.tryParse(), .trim(), .isEmpty, logical operators (||, &&)

Dialogs & Overlays

  • showModalBottomSheet — ฟอร์มเพิ่ม expense (เปิดจากด้านล่าง)
  • showDialog + AlertDialog — แจ้ง validation error
  • ScaffoldMessenger + SnackBar — แจ้งเตือน + Undo ลบ expense (persist: false)
  • Navigator.pop() — ปิด modal/overlay

Swipe to Delete

  • Dismissible + ValueKey — ปัดลบ item ออกจาก list
  • SnackBar Undo — กดย้อนกลับเพิ่ม item คืนได้

Data Modeling

  • enum Category + categoryIcons map — หมวดหมู่แบบ type-safe
  • Initializer list (:) — auto-generate id ด้วย UUID v4
  • ExpenseBucket + forCategory named constructor — จัดกลุ่มค่าใช้จ่ายสำหรับกราฟ

Chart

  • FractionallySizedBox — bar สูงตามสัดส่วน (0.0–1.0) ของค่าใช้จ่ายสูงสุด
  • ExpenseBucket + totalExpenses getter — คำนวณยอดรวมต่อหมวด

สิ่งที่เรียนจาก Section 5 (Theming)

  • ThemeData().copyWith() + ColorScheme.fromSeed() — กำหนด theme ทั้งแอปจากจุดเดียว
  • Dark modeThemeData.dark().copyWith() + kDarkColorScheme
  • MediaQuery — เช็ค platformBrightness สำหรับ dark/light
  • k prefix — convention ตั้งชื่อ global/constant variables (เช่น kColorScheme)
  • CardThemeData — Flutter เวอร์ชันใหม่ใช้แทน CardTheme

สิ่งที่เรียนจาก Section 6 (Responsive & Adaptive)

Widget Size Constraints

  • Widget ถูกกำหนดขนาดจาก parent constraints + child preferences
  • ปัญหา: unconstrained ซ้อน unconstrained (เช่น ListView ใน Column) → ล้นจอ
  • Expanded แก้ปัญหา — เปลี่ยนจาก infinity เป็น “เท่าที่เหลือ”
  • ใช้ DevTools → Layout Explorer ดู constraints ของแต่ละ widget

Responsive Layout

  • MediaQuery.of(context).size.width — เช็คขนาดจอ เลือก Column vs Row
  • LayoutBuilder — รู้ constraints ของ parent (ต่างจาก MediaQuery ที่ดูจอทั้งหมด)
  • Collection if/elseif (width >= 600) Row(...) else Column(...) ใน list ไม่ต้องมี {}
  • เปลี่ยน layout ตาม width: Title+Amount แถวเดียว, Dropdown+Date แถวเดียว

Keyboard & Scroll

  • MediaQuery.of(context).viewInsets.bottom — รู้ว่า keyboard กินพื้นที่เท่าไหร่
  • SingleChildScrollView — ทำให้ modal scroll ได้เมื่อ keyboard เปิด
  • SizedBox(height: double.infinity) — บังคับ modal เต็มจอ

SafeArea

  • useSafeArea: true — ใน showModalBottomSheet หลีก notch/camera อัตโนมัติ
  • Scaffold ใช้ SafeArea ภายในอยู่แล้ว — modal ต้องเปิดเอง

Adaptive (ปรับตาม Platform)

  • Platform.isIOS จาก dart:io — เช็คว่ารันบน iOS หรือ Android
  • CupertinoAlertDialog จาก cupertino.dart — dialog แบบ iOS
  • Flutter ปรับบางอย่างอัตโนมัติ: AppBar title จัดกลางบน iOS, font ต่างกัน

Key Points

  • แอปแรกที่ประกอบ concept หลายตัว: State, Input, Theme, Layout, Overlay, Validation
  • ListView.builder + Dismissible คือ pattern มาตรฐานสำหรับ swipe-to-delete list
  • TextEditingController ต้อง dispose() เสมอ — ไม่งั้น memory leak
  • ThemeData().copyWith() ตั้ง style จากจุดเดียว ไม่ต้อง hardcode สีทั่วแอป
  • Validation ใช้ tryParse (ไม่ throw) + logical operators รวมเงื่อนไข
  • FractionallySizedBox ใช้สัดส่วน 0.0–1.0 แทนค่า pixel ตายตัว
  • Expanded แก้ปัญหา unconstrained ซ้อน unconstrained (ListView ใน Column, Chart ใน Row)
  • LayoutBuilder ดู parent constraints / MediaQuery ดูขนาดจอ — เลือกใช้ตามสถานการณ์
  • Platform.isIOS + CupertinoAlertDialog ทำให้ dialog ดู native บน iOS
  • Flutter — framework ที่ใช้สร้างแอปนี้
  • Widgets — reference ของทุก widget ที่ใช้
  • Dart — ภาษาที่เขียน (enum, initializer list, async/await, tryParse)
  • UUID — ใช้ v4 สร้าง unique ID
  • Responsive Design — Section 6 ปรับ layout ตามขนาดจอ
  • Flutter Academind Complete — สรุปคอร์สทั้งหมด