Flutter - การเขียนโปรแกรมขั้นสูง

ในบทนี้เราจะเรียนรู้วิธีการเขียนแอปพลิเคชันมือถือที่มีคุณสมบัติครบถ้วนคือค่าใช้จ่ายในการคำนวณค่าใช้จ่าย วัตถุประสงค์ของเครื่องคำนวณค่าใช้จ่ายคือการจัดเก็บข้อมูลค่าใช้จ่ายของเรา คุณสมบัติที่สมบูรณ์ของแอปพลิเคชันมีดังนี้ -

  • รายการค่าใช้จ่าย.

  • แบบฟอร์มเพื่อป้อนค่าใช้จ่ายใหม่

  • ตัวเลือกในการแก้ไข / ลบค่าใช้จ่ายที่มีอยู่

  • ค่าใช้จ่ายทั้งหมดในกรณีใด ๆ

เรากำลังจะตั้งโปรแกรมแอปพลิเคชั่น Expense_calculator โดยใช้คุณสมบัติขั้นสูงที่กล่าวถึงด้านล่างของ Flutter framework

  • การใช้ ListView ขั้นสูงเพื่อแสดงรายการค่าใช้จ่าย

  • การเขียนโปรแกรมแบบฟอร์ม

  • การเขียนโปรแกรมฐานข้อมูล SQLite เพื่อจัดเก็บค่าใช้จ่ายของเรา

  • การจัดการสถานะ scoped_model เพื่อลดความซับซ้อนของการเขียนโปรแกรมของเรา

ให้เราเริ่มการเขียนโปรแกรม expense_calculator ใบสมัคร

  • สร้างแอปพลิเคชัน Flutter ใหม่ค่าใช้จ่ายในการคำนวณค่าใช้จ่ายใน Android Studio

  • เปิด pubspec.yaml และเพิ่มการอ้างอิงแพ็คเกจ

dependencies: 
   flutter: 
      sdk: flutter 
   sqflite: ^1.1.0 
   path_provider: ^0.5.0+1 
   scoped_model: ^1.0.1 
   intl: any
  • สังเกตจุดเหล่านี้ที่นี่ -

    • sqflite ใช้สำหรับการเขียนโปรแกรมฐานข้อมูล SQLite

    • path_provider ใช้เพื่อรับพา ธ แอ็พพลิเคชันเฉพาะระบบ

    • scoped_model ใช้สำหรับการจัดการสถานะ

    • intl ใช้สำหรับการจัดรูปแบบวันที่

  • Android studio จะแสดงการแจ้งเตือนต่อไปนี้ว่ามีการอัปเดต pubspec.yaml

  • คลิกตัวเลือกรับการอ้างอิง Android studio จะได้รับแพ็คเกจจากอินเทอร์เน็ตและกำหนดค่าให้เหมาะสมกับแอปพลิเคชัน

  • ลบโค้ดที่มีอยู่ใน main.dart

  • เพิ่มไฟล์ใหม่ Expense.dart เพื่อสร้างคลาส Expense คลาส Expense จะมีคุณสมบัติและวิธีการดังต่อไปนี้

    • property: id - รหัสเฉพาะเพื่อแสดงรายการค่าใช้จ่ายในฐานข้อมูล SQLite

    • property: amount - จำนวนเงินที่ใช้

    • property: date - วันที่ที่มีการใช้จ่ายจำนวนเงิน

    • property: category- หมวดหมู่แสดงถึงพื้นที่ที่มีการใช้จ่ายเงิน เช่นอาหารการเดินทาง ฯลฯ

    • formattedDate - ใช้เพื่อจัดรูปแบบคุณสมบัติวันที่

    • fromMap - ใช้เพื่อแม็พฟิลด์จากตารางฐานข้อมูลไปยังคุณสมบัติในอ็อบเจ็กต์ค่าใช้จ่ายและเพื่อสร้างอ็อบเจ็กต์ค่าใช้จ่ายใหม่

factory Expense.fromMap(Map<String, dynamic> data) { 
   return Expense( 
      data['id'], 
      data['amount'], 
      DateTime.parse(data['date']),    
      data['category'] 
   ); 
}
    • toMap - ใช้ในการแปลงวัตถุค่าใช้จ่ายเป็น Dart Map ซึ่งสามารถใช้เพิ่มเติมในการเขียนโปรแกรมฐานข้อมูล

Map<String, dynamic> toMap() => { 
   "id" : id, 
   "amount" : amount, 
   "date" : date.toString(), 
   "category" : category, 
};
    • columns - ตัวแปรคงที่ใช้แทนฟิลด์ฐานข้อมูล

  • ป้อนและบันทึกรหัสต่อไปนี้ลงในไฟล์ Expense.dart

import 'package:intl/intl.dart'; class Expense {
   final int id; 
   final double amount; 
   final DateTime date; 
   final String category; 
   String get formattedDate { 
      var formatter = new DateFormat('yyyy-MM-dd'); 
      return formatter.format(this.date); 
   } 
   static final columns = ['id', 'amount', 'date', 'category'];
   Expense(this.id, this.amount, this.date, this.category); 
   factory Expense.fromMap(Map<String, dynamic> data) { 
      return Expense( 
         data['id'], 
         data['amount'], 
         DateTime.parse(data['date']), data['category'] 
      ); 
   }
   Map<String, dynamic> toMap() => {
      "id" : id, 
      "amount" : amount, 
      "date" : date.toString(), 
      "category" : category, 
   }; 
}
  • รหัสข้างต้นนั้นง่ายและอธิบายได้ด้วยตนเอง

  • เพิ่มไฟล์ใหม่ Database.dart เพื่อสร้างคลาส SQLiteDbProvider วัตถุประสงค์ของคลาส SQLiteDbProvider มีดังนี้ -

    • รับค่าใช้จ่ายทั้งหมดที่มีอยู่ในฐานข้อมูลโดยใช้เมธอด getAllExpenses จะใช้เพื่อแสดงรายการข้อมูลค่าใช้จ่ายทั้งหมดของผู้ใช้

Future<List<Expense>> getAllExpenses() async { 
   final db = await database; 
   
   List<Map> results = await db.query(
      "Expense", columns: Expense.columns, orderBy: "date DESC"
   );
   List<Expense> expenses = new List(); 
   results.forEach((result) {
      Expense expense = Expense.fromMap(result); 
      expenses.add(expense); 
   }); 
   return expenses; 
}
    • รับข้อมูลค่าใช้จ่ายเฉพาะตามข้อมูลประจำตัวค่าใช้จ่ายที่มีอยู่ในฐานข้อมูลโดยใช้เมธอด getExpenseById จะใช้เพื่อแสดงข้อมูลค่าใช้จ่ายเฉพาะแก่ผู้ใช้

Future<Expense> getExpenseById(int id) async {
   final db = await database;
   var result = await db.query("Expense", where: "id = ", whereArgs: [id]);
   
   return result.isNotEmpty ? 
   Expense.fromMap(result.first) : Null; 
}
    • รับค่าใช้จ่ายทั้งหมดของผู้ใช้โดยใช้เมธอด getTotalExpense จะใช้เพื่อแสดงค่าใช้จ่ายทั้งหมดในปัจจุบันให้กับผู้ใช้

Future<double> getTotalExpense() async {
   final db = await database; 
   List<Map> list = await db.rawQuery(
      "Select SUM(amount) as amount from expense"
   );
   return list.isNotEmpty ? list[0]["amount"] : Null; 
}
    • เพิ่มข้อมูลค่าใช้จ่ายใหม่ลงในฐานข้อมูลโดยใช้วิธีแทรก จะถูกใช้เพื่อเพิ่มรายการค่าใช้จ่ายใหม่ลงในแอปพลิเคชันโดยผู้ใช้

Future<Expense> insert(Expense expense) async { 
   final db = await database; 
   var maxIdResult = await db.rawQuery(
      "SELECT MAX(id)+1 as last_inserted_id FROM Expense"
   );
   var id = maxIdResult.first["last_inserted_id"]; 
   var result = await db.rawInsert(
      "INSERT Into Expense (id, amount, date, category)" 
      " VALUES (?, ?, ?, ?)", [
         id, expense.amount, expense.date.toString(), expense.category
      ]
   ); 
   return Expense(id, expense.amount, expense.date, expense.category); 
}
    • อัปเดตข้อมูลค่าใช้จ่ายที่มีอยู่โดยใช้วิธีการอัปเดต จะใช้เพื่อแก้ไขและอัปเดตรายการค่าใช้จ่ายที่มีอยู่ในระบบโดยผู้ใช้

update(Expense product) async {
   final db = await database; 
   
   var result = await db.update("Expense", product.toMap(), 
   where: "id = ?", whereArgs: [product.id]); 
   return result; 
}
    • ลบข้อมูลค่าใช้จ่ายที่มีอยู่โดยใช้วิธีลบ จะถูกใช้ลบรายการค่าใช้จ่ายที่มีอยู่ในระบบโดยผู้ใช้

delete(int id) async {
   final db = await database;
   db.delete("Expense", where: "id = ?", whereArgs: [id]); 
}
  • โค้ดทั้งหมดของคลาส SQLiteDbProvider มีดังนี้ -

import 'dart:async'; 
import 'dart:io'; 
import 'package:path/path.dart'; 
import 'package:path_provider/path_provider.dart'; 
import 'package:sqflite/sqflite.dart'; 
import 'Expense.dart'; 

class SQLiteDbProvider {
   SQLiteDbProvider._(); 
   static final SQLiteDbProvider db = SQLiteDbProvider._(); 
   
   static Database _database; Future<Database> get database async { 
      if (_database != null) 
         return _database; 
      _database = await initDB(); 
      return _database; 
   } 
   initDB() async {
      Directory documentsDirectory = await getApplicationDocumentsDirectory(); 
      String path = join(documentsDirectory.path, "ExpenseDB2.db"); 
      return await openDatabase(
         path, version: 1, onOpen:(db){}, onCreate: (Database db, int version) async {
            await db.execute(
               "CREATE TABLE Expense (
                  ""id INTEGER PRIMARY KEY," "amount REAL," "date TEXT," "category TEXT""
               )
            "); 
            await db.execute(
               "INSERT INTO Expense ('id', 'amount', 'date', 'category') 
               values (?, ?, ?, ?)",[1, 1000, '2019-04-01 10:00:00', "Food"]
            );
            /*await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", [
                  2, "Pixel", "Pixel is the most feature phone ever", 800, "pixel.png"
               ]
            ); 
            await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", [
                  3, "Laptop", "Laptop is most productive development tool", 2000, "laptop.png"
               ]
            );
            await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", [
                  4, "Tablet", "Laptop is most productive development tool", 1500, "tablet.png"
               ]
            );
            await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", [
                  5, "Pendrive", "iPhone is the stylist phone ever", 100, "pendrive.png"
               ]
            ); 
            await db.execute(
               "INSERT INTO Product ('id', 'name', 'description', 'price', 'image') 
               values (?, ?, ?, ?, ?)", [
                  6, "Floppy Drive", "iPhone is the stylist phone ever", 20, "floppy.png"
               ]
            ); */ 
         }
      );
   }
   Future<List<Expense>> getAllExpenses() async {
      final db = await database; 
      List<Map> 
      results = await db.query(
         "Expense", columns: Expense.columns, orderBy: "date DESC"
      );
      List<Expense> expenses = new List(); 
      results.forEach((result) {
         Expense expense = Expense.fromMap(result);
         expenses.add(expense);
      }); 
      return expenses; 
   } 
   Future<Expense> getExpenseById(int id) async {
      final db = await database;
      var result = await db.query("Expense", where: "id = ", whereArgs: [id]); 
      return result.isNotEmpty ? Expense.fromMap(result.first) : Null; 
   }
   Future<double> getTotalExpense() async {
      final db = await database;
      List<Map> list = await db.rawQuery(
         "Select SUM(amount) as amount from expense"
      );
      return list.isNotEmpty ? list[0]["amount"] : Null; 
   }
   Future<Expense> insert(Expense expense) async {
      final db = await database; 
      var maxIdResult = await db.rawQuery(
         "SELECT MAX(id)+1 as last_inserted_id FROM Expense"
      );
      var id = maxIdResult.first["last_inserted_id"]; 
      var result = await db.rawInsert(
         "INSERT Into Expense (id, amount, date, category)" 
         " VALUES (?, ?, ?, ?)", [
            id, expense.amount, expense.date.toString(), expense.category
         ]
      );
      return Expense(id, expense.amount, expense.date, expense.category); 
   }
   update(Expense product) async {
      final db = await database; 
      var result = await db.update(
         "Expense", product.toMap(), where: "id = ?", whereArgs: [product.id]
      ); 
      return result; 
   }
   delete(int id) async {
      final db = await database;
      db.delete("Expense", where: "id = ?", whereArgs: [id]);
   }
}
  • Here,

    • ฐานข้อมูลเป็นคุณสมบัติในการรับวัตถุ SQLiteDbProvider

    • initDB เป็นวิธีการที่ใช้ในการเลือกและเปิดฐานข้อมูล SQLite

  • สร้างไฟล์ใหม่ ExpenseListModel.dart เพื่อสร้าง ExpenseListModel วัตถุประสงค์ของแบบจำลองคือเพื่อเก็บข้อมูลที่สมบูรณ์ของค่าใช้จ่ายของผู้ใช้ไว้ในหน่วยความจำและอัปเดตส่วนติดต่อผู้ใช้ของแอปพลิเคชันเมื่อใดก็ตามที่ค่าใช้จ่ายของผู้ใช้เปลี่ยนแปลงในหน่วยความจำ มันขึ้นอยู่กับคลาสโมเดลจากแพ็คเกจ scoped_model มีคุณสมบัติและวิธีการดังนี้ -

    • _items - รายการค่าใช้จ่ายส่วนตัว

    • items - เริ่มให้ _items เป็น UnmodifiableListView <Expense> เพื่อป้องกันการเปลี่ยนแปลงรายการโดยไม่คาดคิดหรือโดยบังเอิญ

    • totalExpense - getter สำหรับค่าใช้จ่ายรวมตามตัวแปรรายการ

double get totalExpense {
   double amount = 0.0; 
   for(var i = 0; i < _items.length; i++) { 
      amount = amount + _items[i].amount; 
   } 
   return amount; 
}
    • load - ใช้เพื่อโหลดค่าใช้จ่ายทั้งหมดจากฐานข้อมูลและลงในตัวแปร _items นอกจากนี้ยังเรียกใช้ alertListeners เพื่ออัปเดต UI

void load() {
   Future<List<Expense>> 
   list = SQLiteDbProvider.db.getAllExpenses(); 
   list.then( (dbItems) {
      for(var i = 0; i < dbItems.length; i++) { 
         _items.add(dbItems[i]); 
      } notifyListeners(); 
   });
}
    • byId - ใช้เพื่อรับค่าใช้จ่ายเฉพาะจากตัวแปร _items

Expense byId(int id) { 
   for(var i = 0; i < _items.length; i++) { 
      if(_items[i].id == id) { 
         return _items[i]; 
      } 
   }
   return null; 
}
    • เพิ่ม - ใช้เพื่อเพิ่มรายการค่าใช้จ่ายใหม่ลงในตัวแปร _items รวมทั้งในฐานข้อมูล นอกจากนี้ยังเรียกใช้ alertListeners เพื่ออัปเดต UI

void add(Expense item) {
   SQLiteDbProvider.db.insert(item).then((val) { 
      _items.add(val); notifyListeners(); 
   }); 
}
    • อัปเดต - ใช้เพื่ออัปเดตรายการค่าใช้จ่ายในตัวแปร _items รวมทั้งในฐานข้อมูล นอกจากนี้ยังเรียกใช้ alertListeners เพื่ออัปเดต UI

void update(Expense item) {
   bool found = false;
   for(var i = 0; i < _items.length; i++) {
      if(_items[i].id == item.id) {
         _items[i] = item; 
         found = true; 
         SQLiteDbProvider.db.update(item); break; 
      } 
   }
   if(found) notifyListeners(); 
}
    • ลบ - ใช้เพื่อลบรายการค่าใช้จ่ายที่มีอยู่ในตัวแปร _items รวมทั้งจากฐานข้อมูล นอกจากนี้ยังเรียกใช้ alertListeners เพื่ออัปเดต UI

void delete(Expense item) { 
   bool found = false; 
   for(var i = 0; i < _items.length; i++) {
      if(_items[i].id == item.id) {
         found = true; 
         SQLiteDbProvider.db.delete(item.id); 
         _items.removeAt(i); break; 
      }
   }
   if(found) notifyListeners(); 
}
  • รหัสที่สมบูรณ์ของคลาส ExpenseListModel มีดังนี้ -

import 'dart:collection'; 
import 'package:scoped_model/scoped_model.dart'; 
import 'Expense.dart'; 
import 'Database.dart'; 

class ExpenseListModel extends Model { 
   ExpenseListModel() { 
      this.load(); 
   } 
   final List<Expense> _items = []; 
   UnmodifiableListView<Expense> get items => 
   UnmodifiableListView(_items); 
   
   /*Future<double> get totalExpense { 
      return SQLiteDbProvider.db.getTotalExpense(); 
   }*/ 
   
   double get totalExpense {
      double amount = 0.0;
      for(var i = 0; i < _items.length; i++) { 
         amount = amount + _items[i].amount; 
      } 
      return amount; 
   }
   void load() {
      Future<List<Expense>> list = SQLiteDbProvider.db.getAllExpenses(); 
      list.then( (dbItems) {
         for(var i = 0; i < dbItems.length; i++) {
            _items.add(dbItems[i]); 
         } 
         notifyListeners(); 
      }); 
   }
   Expense byId(int id) {
      for(var i = 0; i < _items.length; i++) { 
         if(_items[i].id == id) { 
            return _items[i]; 
         } 
      }
      return null; 
   }
   void add(Expense item) {
      SQLiteDbProvider.db.insert(item).then((val) {
         _items.add(val);
         notifyListeners();
      }); 
   }
   void update(Expense item) {
      bool found = false; 
      for(var i = 0; i < _items.length; i++) {
         if(_items[i].id == item.id) {
            _items[i] = item; 
            found = true; 
            SQLiteDbProvider.db.update(item); 
            break; 
         }
      }
      if(found) notifyListeners(); 
   }
   void delete(Expense item) {
      bool found = false; 
      for(var i = 0; i < _items.length; i++) {
         if(_items[i].id == item.id) {
            found = true; 
            SQLiteDbProvider.db.delete(item.id); 
            _items.removeAt(i); break; 
         }
      }
      if(found) notifyListeners(); 
   }
}
  • เปิดไฟล์ main.dart นำเข้าคลาสตามที่ระบุด้านล่าง -

import 'package:flutter/material.dart'; 
import 'package:scoped_model/scoped_model.dart'; 
import 'ExpenseListModel.dart'; 
import 'Expense.dart';
  • เพิ่มฟังก์ชันหลักและเรียกใช้ runApp โดยส่งผ่านวิดเจ็ต ScopedModel <ExpenseListModel>

void main() { 
   final expenses = ExpenseListModel(); 
   runApp(
      ScopedModel<ExpenseListModel>(model: expenses, child: MyApp(),)
   );
}
  • Here,

    • วัตถุค่าใช้จ่ายโหลดข้อมูลค่าใช้จ่ายผู้ใช้ทั้งหมดจากฐานข้อมูล นอกจากนี้เมื่อเปิดแอปพลิเคชันเป็นครั้งแรกแอปพลิเคชันจะสร้างฐานข้อมูลที่ต้องการพร้อมตารางที่เหมาะสม

    • ScopedModel ให้ข้อมูลค่าใช้จ่ายในช่วงอายุการใช้งานทั้งหมดของแอปพลิเคชันและรับประกันการบำรุงรักษาสถานะของแอปพลิเคชันที่อินสแตนซ์ใด ๆ ช่วยให้เราสามารถใช้ StatelessWidget แทน StatefulWidget

  • สร้าง MyApp ง่ายๆโดยใช้วิดเจ็ต MaterialApp

class MyApp extends StatelessWidget {
   // This widget is the root of your application. 
   @override 
   Widget build(BuildContext context) {
      return MaterialApp(
         title: 'Expense',
         theme: ThemeData(
            primarySwatch: Colors.blue, 
         ), 
         home: MyHomePage(title: 'Expense calculator'), 
      );
   }
}
  • สร้างวิดเจ็ต MyHomePage เพื่อแสดงข้อมูลค่าใช้จ่ายทั้งหมดของผู้ใช้พร้อมกับค่าใช้จ่ายทั้งหมดที่ด้านบน ปุ่มลอยที่มุมล่างขวาจะถูกใช้เพื่อเพิ่มค่าใช้จ่ายใหม่

class MyHomePage extends StatelessWidget { 
   MyHomePage({Key key, this.title}) : super(key: key); 
   final String title; 
   @override 
   Widget build(BuildContext context) {
      return Scaffold(
         appBar: AppBar( 
            title: Text(this.title), 
         ), 
         body: ScopedModelDescendant<ExpenseListModel>(
            builder: (context, child, expenses) {
               return ListView.separated(
                  itemCount: expenses.items == null ? 1 
                  : expenses.items.length + 1, 
                  itemBuilder: (context, index) { 
                     if (index == 0) { 
                        return ListTile(
                           title: Text("Total expenses: " 
                           + expenses.totalExpense.toString(), 
                           style: TextStyle(fontSize: 24,
                           fontWeight: FontWeight.bold),) 
                        );
                     } else {
                        index = index - 1; 
                        return Dismissible( 
                           key: Key(expenses.items[index].id.toString()), 
                              onDismissed: (direction) { 
                              expenses.delete(expenses.items[index]); 
                              Scaffold.of(context).showSnackBar(
                                 SnackBar(
                                    content: Text(
                                       "Item with id, " 
                                       + expenses.items[index].id.toString() + 
                                       " is dismissed"
                                    )
                                 )
                              ); 
                           },
                           child: ListTile( onTap: () { 
                              Navigator.push(
                                 context, MaterialPageRoute(
                                    builder: (context) => FormPage(
                                       id: expenses.items[index].id,
                                       expenses: expenses, 
                                    )
                                 )
                              );
                           }, 
                           leading: Icon(Icons.monetization_on), 
                           trailing: Icon(Icons.keyboard_arrow_right), 
                           title: Text(expenses.items[index].category + ": " + 
                           expenses.items[index].amount.toString() + 
                           " \nspent on " + expenses.items[index].formattedDate, 
                           style: TextStyle(fontSize: 18, fontStyle: FontStyle.italic),))
                        ); 
                     }
                  },
                  separatorBuilder: (context, index) { 
                     return Divider(); 
                  }, 
               );
            },
         ),
         floatingActionButton: ScopedModelDescendant<ExpenseListModel>(
            builder: (context, child, expenses) {
               return FloatingActionButton( onPressed: () {
                  Navigator.push( 
                     context, MaterialPageRoute(
                        builder: (context) => ScopedModelDescendant<ExpenseListModel>(
                           builder: (context, child, expenses) { 
                              return FormPage( id: 0, expenses: expenses, ); 
                           }
                        )
                     )
                  ); 
                  // expenses.add(new Expense( 
                     // 2, 1000, DateTime.parse('2019-04-01 11:00:00'), 'Food')
                  ); 
                  // print(expenses.items.length); 
               },
               tooltip: 'Increment', child: Icon(Icons.add), ); 
            }
         )
      );
   }
}
  • Here,

    • ScopedModelDescendant ใช้เพื่อส่งผ่านโมเดลค่าใช้จ่ายไปยังวิดเจ็ต ListView และ FloatingActionButton

    • ListView.separated และวิดเจ็ต ListTile ใช้เพื่อแสดงรายการข้อมูลค่าใช้จ่าย

    • วิดเจ็ตที่ปิดไม่ได้ใช้เพื่อลบรายการค่าใช้จ่ายโดยใช้ท่าทางรูด

    • Navigator ใช้เพื่อเปิดอินเตอร์เฟสแก้ไขของรายการค่าใช้จ่าย สามารถเปิดใช้งานได้โดยแตะรายการค่าใช้จ่าย

  • สร้างวิดเจ็ต FormPage วัตถุประสงค์ของวิดเจ็ต FormPage คือการเพิ่มหรืออัพเดตรายการค่าใช้จ่าย จัดการการตรวจสอบรายการค่าใช้จ่ายด้วย

class FormPage extends StatefulWidget { 
   FormPage({Key key, this.id, this.expenses}) : super(key: key); 
   final int id; 
   final ExpenseListModel expenses; 
   
   @override _FormPageState createState() => _FormPageState(id: id, expenses: expenses); 
}
class _FormPageState extends State<FormPage> {
   _FormPageState({Key key, this.id, this.expenses}); 
   
   final int id; 
   final ExpenseListModel expenses; 
   final scaffoldKey = GlobalKey<ScaffoldState>(); 
   final formKey = GlobalKey<FormState>(); 
   
   double _amount; 
   DateTime _date; 
   String _category; 
   
   void _submit() {
      final form = formKey.currentState; 
      if (form.validate()) {
         form.save(); 
         if (this.id == 0) expenses.add(Expense(0, _amount, _date, _category)); 
            else expenses.update(Expense(this.id, _amount, _date, _category)); 
         Navigator.pop(context); 
      }
   }
   @override 
   Widget build(BuildContext context) {
      return Scaffold(
         key: scaffoldKey, appBar: AppBar(
            title: Text('Enter expense details'),
         ), 
         body: Padding(
            padding: const EdgeInsets.all(16.0), 
            child: Form(
               key: formKey, child: Column(
                  children: [
                     TextFormField( 
                        style: TextStyle(fontSize: 22), 
                        decoration: const InputDecoration( 
                           icon: const Icon(Icons.monetization_on), 
                           labelText: 'Amount', 
                           labelStyle: TextStyle(fontSize: 18)
                        ), 
                        validator: (val) {
                           Pattern pattern = r'^[1-9]\d*(\.\d+)?$'; 
                           RegExp regex = new RegExp(pattern); 
                           if (!regex.hasMatch(val)) 
                           return 'Enter a valid number'; else return null; 
                        }, 
                        initialValue: id == 0 
                        ? '' : expenses.byId(id).amount.toString(), 
                        onSaved: (val) => _amount = double.parse(val), 
                     ), 
                     TextFormField( 
                        style: TextStyle(fontSize: 22), 
                        decoration: const InputDecoration( 
                           icon: const Icon(Icons.calendar_today),
                           hintText: 'Enter date', 
                           labelText: 'Date', 
                           labelStyle: TextStyle(fontSize: 18), 
                        ), 
                        validator: (val) {
                           Pattern pattern = r'^((?:19|20)\d\d)[- /.]
                              (0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$'; 
                           RegExp regex = new RegExp(pattern); 
                           if (!regex.hasMatch(val)) 
                              return 'Enter a valid date'; 
                           else return null; 
                        },
                        onSaved: (val) => _date = DateTime.parse(val), 
                        initialValue: id == 0 
                        ? '' : expenses.byId(id).formattedDate, 
                        keyboardType: TextInputType.datetime, 
                     ),
                     TextFormField(
                        style: TextStyle(fontSize: 22), 
                        decoration: const InputDecoration(
                           icon: const Icon(Icons.category),
                           labelText: 'Category', 
                           labelStyle: TextStyle(fontSize: 18)
                        ),
                        onSaved: (val) => _category = val, 
                        initialValue: id == 0 ? '' 
                        : expenses.byId(id).category.toString(),
                     ), 
                     RaisedButton( 
                        onPressed: _submit, 
                        child: new Text('Submit'), 
                     ), 
                  ],
               ),
            ),
         ),
      );
   }
}
  • Here,

    • TextFormField ใช้เพื่อสร้างรายการแบบฟอร์ม

    • คุณสมบัติตัวตรวจสอบความถูกต้องของ TextFormField ถูกใช้เพื่อตรวจสอบความถูกต้องขององค์ประกอบฟอร์มพร้อมกับรูปแบบ RegEx

    • ฟังก์ชัน _submit ถูกใช้ร่วมกับวัตถุค่าใช้จ่ายเพื่อเพิ่มหรืออัปเดตค่าใช้จ่ายลงในฐานข้อมูล

  • รหัสที่สมบูรณ์ของไฟล์ main.dart มีดังนี้ -

import 'package:flutter/material.dart'; 
import 'package:scoped_model/scoped_model.dart'; 
import 'ExpenseListModel.dart'; 
import 'Expense.dart'; 

void main() { 
   final expenses = ExpenseListModel(); 
   runApp(
      ScopedModel<ExpenseListModel>(
         model: expenses, child: MyApp(), 
      )
   ); 
}
class MyApp extends StatelessWidget {
   // This widget is the root of your application. 
   @override
   Widget build(BuildContext context) {
      return MaterialApp(
         title: 'Expense',
         theme: ThemeData(
            primarySwatch: Colors.blue, 
         ), 
         home: MyHomePage(title: 'Expense calculator'), 
      );
   }
}
class MyHomePage extends StatelessWidget {
   MyHomePage({Key key, this.title}) : super(key: key);
   final String title;

   @override 
   Widget build(BuildContext context) {
      return Scaffold(
         appBar: AppBar(
            title: Text(this.title),
         ),
         body: ScopedModelDescendant<ExpenseListModel>(
            builder: (context, child, expenses) { 
               return ListView.separated(
                  itemCount: expenses.items == null ? 1 
                  : expenses.items.length + 1, itemBuilder: (context, index) { 
                     if (index == 0) { 
                        return ListTile( title: Text("Total expenses: " 
                        + expenses.totalExpense.toString(), 
                        style: TextStyle(fontSize: 24,fontWeight: 
                        FontWeight.bold),) ); 
                     } else {
                        index = index - 1; return Dismissible(
                           key: Key(expenses.items[index].id.toString()), 
                           onDismissed: (direction) {
                              expenses.delete(expenses.items[index]); 
                              Scaffold.of(context).showSnackBar(
                                 SnackBar(
                                    content: Text(
                                       "Item with id, " + 
                                       expenses.items[index].id.toString() 
                                       + " is dismissed"
                                    )
                                 )
                              );
                           }, 
                           child: ListTile( onTap: () {
                              Navigator.push( context, MaterialPageRoute(
                                 builder: (context) => FormPage(
                                    id: expenses.items[index].id, expenses: expenses, 
                                 )
                              ));
                           }, 
                           leading: Icon(Icons.monetization_on), 
                           trailing: Icon(Icons.keyboard_arrow_right), 
                           title: Text(expenses.items[index].category + ": " + 
                           expenses.items[index].amount.toString() + " \nspent on " + 
                           expenses.items[index].formattedDate, 
                           style: TextStyle(fontSize: 18, fontStyle: FontStyle.italic),))
                        );
                     }
                  }, 
                  separatorBuilder: (context, index) {
                     return Divider(); 
                  },
               ); 
            },
         ),
         floatingActionButton: ScopedModelDescendant<ExpenseListModel>(
            builder: (context, child, expenses) {
               return FloatingActionButton(
                  onPressed: () {
                     Navigator.push(
                        context, MaterialPageRoute(
                           builder: (context)
                           => ScopedModelDescendant<ExpenseListModel>(
                              builder: (context, child, expenses) { 
                                 return FormPage( id: 0, expenses: expenses, ); 
                              }
                           )
                        )
                     );
                     // expenses.add(
                        new Expense(
                           // 2, 1000, DateTime.parse('2019-04-01 11:00:00'), 'Food'
                        )
                     );
                     // print(expenses.items.length); 
                  },
                  tooltip: 'Increment', child: Icon(Icons.add), 
               );
            }
         )
      );
   } 
}
class FormPage extends StatefulWidget {
   FormPage({Key key, this.id, this.expenses}) : super(key: key); 
   final int id; 
   final ExpenseListModel expenses; 
   
   @override 
   _FormPageState createState() => _FormPageState(id: id, expenses: expenses); 
}
class _FormPageState extends State<FormPage> {
   _FormPageState({Key key, this.id, this.expenses}); 
   final int id; 
   final ExpenseListModel expenses; 
   final scaffoldKey = GlobalKey<ScaffoldState>(); 
   final formKey = GlobalKey<FormState>(); 
   double _amount; DateTime _date; 
   String _category;
   void _submit() {
      final form = formKey.currentState; 
      if (form.validate()) {
         form.save(); 
         if (this.id == 0) expenses.add(Expense(0, _amount, _date, _category)); 
         else expenses.update(Expense(this.id, _amount, _date, _category)); 
         Navigator.pop(context); 
      } 
   } 
   @override 
   Widget build(BuildContext context) {
      return Scaffold(
         key: scaffoldKey, appBar: AppBar( 
            title: Text('Enter expense details'), 
         ), 
         body: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Form(
               key: formKey, child: Column(
                  children: [
                     TextFormField(
                        style: TextStyle(fontSize: 22), 
                        decoration: const InputDecoration( 
                           icon: const Icon(Icons.monetization_on), 
                           labelText: 'Amount', 
                           labelStyle: TextStyle(fontSize: 18)
                        ), 
                        validator: (val) {
                           Pattern pattern = r'^[1-9]\d*(\.\d+)?$'; 
                           RegExp regex = new RegExp(pattern); 
                           if (!regex.hasMatch(val)) return 'Enter a valid number'; 
                           else return null; 
                        },
                        initialValue: id == 0 ? '' 
                        : expenses.byId(id).amount.toString(), 
                        onSaved: (val) => _amount = double.parse(val), 
                     ),
                     TextFormField(
                        style: TextStyle(fontSize: 22), 
                        decoration: const InputDecoration(
                           icon: const Icon(Icons.calendar_today), 
                           hintText: 'Enter date', 
                           labelText: 'Date', 
                           labelStyle: TextStyle(fontSize: 18), 
                        ),
                        validator: (val) {
                           Pattern pattern = r'^((?:19|20)\d\d)[- /.]
                           (0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$'; 
                           RegExp regex = new RegExp(pattern); 
                           if (!regex.hasMatch(val)) return 'Enter a valid date'; 
                           else return null; 
                        },
                        onSaved: (val) => _date = DateTime.parse(val), 
                        initialValue: id == 0 ? '' : expenses.byId(id).formattedDate, 
                        keyboardType: TextInputType.datetime, 
                     ),
                     TextFormField(
                        style: TextStyle(fontSize: 22), 
                        decoration: const InputDecoration(
                           icon: const Icon(Icons.category), 
                           labelText: 'Category', 
                           labelStyle: TextStyle(fontSize: 18)
                        ), 
                        onSaved: (val) => _category = val, 
                        initialValue: id == 0 ? '' : expenses.byId(id).category.toString(), 
                     ),
                     RaisedButton(
                        onPressed: _submit, 
                        child: new Text('Submit'), 
                     ),
                  ],
               ),
            ),
         ),
      );
   }
}
  • ตอนนี้เรียกใช้แอปพลิเคชัน

  • เพิ่มค่าใช้จ่ายใหม่โดยใช้ปุ่มลอย

  • แก้ไขค่าใช้จ่ายที่มีอยู่โดยแตะรายการค่าใช้จ่าย

  • ลบค่าใช้จ่ายที่มีอยู่โดยการปัดรายการค่าใช้จ่ายในทิศทางใดทิศทางหนึ่ง

ภาพหน้าจอบางส่วนของแอปพลิเคชันมีดังนี้ -