ต่อเนื่องจากเเรื่องที่แล้ว ในการใช้งาน Provider และ Comsmer ในการนำค่าจากการป้อนผ่านฟอร์มมาแสดงผลใน List View ซึ่งเป็นการเก็บข้อมูลในโครงสร้างข้อมูล List ปัญหาคือเมื่อปิดแอปแล้วเปิดใหม่ ค่าที่เคยบันทึกก็จะสูญหายไปด้วย ดังนั้น ในการใช้งานจริงเราก็จะทำการบันทึกข้อมูลลง Local Database ใยอุปกรณ์ของเรา เพื่อที่จะสามารถนำค่าที่เคยบันทึกมาแสดงได้ แต่ทั้งนี้ทั้งนั้น หากทำการถอนแอปออก ข้อมูลดังกล่าวก็จะหายไปด้วย

 วันนี้เลยได้ลองดูคลิปการสอน Flutter KongRuksiam ตอน “พัฒนาแอพด้วย Flutter & Database จัดการฐานข้อมูล [Full Course]” ต่อจาก ซึ่งได้พูดถึง Database แบบ SQL และ NoSQL ซึ่ง มีรูปแบบการใช้งานที่แตกต่างกันบ้างในบางอย่าง ดังนั้นรูปภาพประกอบทั้งหมดอ้างอิงมาจาก คลิปการสอนของ KongRulsiam

แต่รูปแบบการจัดเก็บคล้ายๆกันบ้าง

การจัดการข้อมูลแบบ NoSQL

ขั้นตอนการทำงาน

จากรูปด้านบนจะเป็นขั้นตอนการทำงานเกี่ยวกับ Database โดยจะเกี่ยวข้องกับพื้นที่จัดเก็บข้อมูลในอุปกรณ์ โดยใช้ Library path และ Path_provider จากนั้นสร้าง Database สำหรับจัดเก็บหรือเปิดฐานข้อมูลเพื่อใช้งาน และทำการสร้าง Store เพื่อจัดเก็บข้อมูล โดยจัดเก็บในรูปแบบ JSON จากนั้นจะ Return Key กลับคืน

จากผังการทำงาน นำมาสร้างเป็นโปรเจ๊คตัวอย่างต่อจาก Provider และ Consumer ดังรูปด้านบน เปิดหน้าแรก แสดงข้อมูลจาก Database และ หน้าที่สองจะเป็นการเพิ่มข้อมูลรายการ แล้วบันทึกเก็บไว้ที่ Database

  1. ไฟล์ Main.dart
import 'package:examdatabase/model/transaction.dart';
import 'package:examdatabase/providers/transaction_provider.dart';
import 'package:examdatabase/screens/form_screen.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) {
          return TransactionProvider();
        })
      ],
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.purple,
        ),
        home: const MyHomePage(title: 'Example Database'),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    //เรียกดึงข้อมูลเมื่อเปิดใช้ App
    Provider.of<TransactionProvider>(context, listen: false).initData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          actions: [
            IconButton(
                onPressed: () {
                  Navigator.push(context, MaterialPageRoute(builder: (context) {
                    return FormScreen();
                  }));
                },
                icon: Icon(Icons.add))
          ],
          title: Text(widget.title),
        ),
        body: Consumer(
          builder: (context, TransactionProvider provider, Widget? child) {
            var count = provider.transactions.length;
            if (count <= 0) {
              return Center(
                child: Text(
                  "ไม่พบข้อมูล",
                  style: TextStyle(fontSize: 35),
                ),
              );
            } else {
              return ListView.builder(
                  itemCount: count,
                  itemBuilder: (context, int index) {
                    Transactions data = provider.transactions[index];
                    return Card(
                      elevation: 10,
                      margin: EdgeInsets.symmetric(vertical: 5, horizontal: 15),
                      child: ListTile(
                        leading: CircleAvatar(
                          child: FittedBox(
                              child: Text(
                            data.amount.toString(),
                            style: TextStyle(
                                fontSize: 14, fontWeight: FontWeight.bold),
                          )),
                          radius: 30,
                        ),
                        title: Text(data.title),
                        subtitle: Text(
                            DateFormat("dd/MM/yyyy H:m").format(data.date)),
                      ),
                    );
                  });
            }
          },
        ));
  }
}

2.ไฟล์ Transaction.dart // Map List

class Transactions {
  String title; // ชื่อรายการ
  double amount; // จำนวนเงิน
  DateTime date; // วันวลา บันทึกรายการ

  Transactions({required this.title, required this.amount, required this.date});
}

3.ไฟล์ Transaction_provider.dart

import 'package:examdatabase/database/transaction_db.dart';
import 'package:examdatabase/model/transaction.dart';
import 'package:flutter/foundation.dart';

class TransactionProvider with ChangeNotifier {
  //  ตัวอย่างข้อมูล
  List<Transactions> transactions = [];

  List<Transactions> getTransaction() {
    return transactions;
  }
//ดึงข้อมูลจาก Database มาแสดงเมื่อทำการเปิด App
  void initData() async{
    var db = TransactionDB(dbname: "transaction.db");
    transactions= await db.loadAllData();
    notifyListeners();
  }
  //บันทึกข้อมูลจากฟอร์มลง Database
  void addTransaction(Transactions statement) async {
    var db = TransactionDB(dbname: "transaction.db");
    //บันทึกข้อมูล 
    await db.InsertData(statement);
    //ดึงข้อมูลมาแสดงผล
    transactions= await db.loadAllData();
    //แจ้งเตือน Consumer
    notifyListeners();
  }
}

4.ไฟล์ transaction_db.dart

import 'dart:async';
import 'dart:io';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart';

import '../model/transaction.dart';

class TransactionDB {
  //บริการฐานข้อมูล  เปิด ปิด เพิ่มลบ แก้ไข ฐานข้อมูล
  String? dbname; //เก็บชื่อบานข้อมูล

  //ถ้ายังไม่ถูกสร้าง ให้ ทำการสร้าง
  //ถูกสร้างไว้แล้ว ให้ เปิด
  TransactionDB({this.dbname});

  Future<Database> openDatabase() async {
    //หาตำแหน่งที่จัดเก็บข้อมูล
    Directory appDirectory =
        await getApplicationDocumentsDirectory(); // ดึงตำแหน่งจัดเก็บ Application ในอุปกรณ์
    String dbLocation =
        join(appDirectory.path, dbname); //กำหนด Database Directory
    //สร้าง database
    DatabaseFactory dbFactory = await databaseFactoryIo; //สร้าง db
    Database db = await dbFactory.openDatabase(dbLocation); //สร้าง db
    return db;
  }

  //บันทึกข้อมูล
  Future<int> InsertData(Transactions statement) async {
    //ฐานข้อมูล => Store
    //transaction.db => expense
    var db = await this.openDatabase(); //สร้าง Database
    var store = intMapStoreFactory.store("expense"); // สร้าง Store

    //json รูปแบบข้อมูลที่จะบันทึก บันทึกข้อมูลในรูปแบบ json และ Return ID
    var keyID = store.add(db, {
      "title": statement.title,
      "amount": statement.amount,
      "date": statement.date.toIso8601String()
    });
    db.close();
    return keyID;
  }

//ดึงข้อมูล
  Future<List<Transactions>> loadAllData() async {
    var db = await this.openDatabase(); //สร้าง Database
    var store = intMapStoreFactory.store("expense"); // สร้าง Store
    // var snapshot = await store.find(db); // ดึงและเรียงจากก่อนไปหลัง โดยไม่มีการกำหนดค่าใดๆ
    var snapshot = await store.find(db,
        finder: Finder(sortOrders: [
          SortOrder(Field.key, false)
        ])); // ดึงและเรียงจากหลังไปก่อน true เรียงจาก น้อยไปมาก
    // List transactionList = List<Transactions>();
    List<Transactions> transactionList = [];
    for (var record in snapshot) {
      transactionList.add(Transactions(
          title: record["title"].toString(),
          amount: double.parse(record["amount"].toString()),
          date: DateTime.parse(record["date"].toString())));
    }
    print(snapshot);

    return transactionList;
  }
}

5.ไฟล์ form_screen.dart

import 'package:examdatabase/model/transaction.dart';
import 'package:examdatabase/providers/transaction_provider.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class FormScreen extends StatelessWidget {
  // const FormScreen({super.key});
  final formkey = GlobalKey<FormState>();
  // Controller
  final titleController = TextEditingController();
  final amountController = TextEditingController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('แบบฟอร์มบันทึกข้อมูล'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(10.0),
          child: Form(
              key: formkey,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.start,
                children: [
                  TextFormField(
                    controller: titleController,
                    decoration: InputDecoration(label: Text('ชื่อรายการ')),
                    autofocus: true,
                    validator: (String? str) {
                      if (str!.isEmpty) {
                        return "กรุณาป้อนชื่อรายการ";
                      }
                      return null;
                    },
                  ),
                  TextFormField(
                    controller: amountController,
                    decoration: InputDecoration(label: Text('จำนวนเงิน')),
                    keyboardType: TextInputType.number,
                    validator: (String? str) {
                      if (str!.isEmpty) {
                        return "กรุณาป้อนจำนวนเงิน";
                      }
                      if (double.parse(str) <= 0) {
                        return "กรุณาป้อนตัวเลขมากกว่า 0";
                      }
                      return null;
                    },
                  ),
                  SizedBox(
                    height: 5,
                  ),
                  TextButton(
                      child: Text('เพิ่มข้อมูล'),
                      onPressed: () {
                        if (formkey.currentState!.validate()) {
                          var title = titleController.text;
                          var amount = amountController.text;

                          print('$title   and $amount');
                          // เตรียมข้มูล
                          Transactions statement = Transactions(
                              title: title,
                              amount: double.parse(amount),
                              date: DateTime.now());
                          // เรียก provider
                          var provider = Provider.of<TransactionProvider>(
                              context,
                              listen: false);
                          provider.addTransaction(statement);
                          Navigator.pop(context);
                        }
                      },
                      style: TextButton.styleFrom(
                          backgroundColor: Colors.purple,
                          foregroundColor: Colors.white,
                          shadowColor: Colors.black))
                ],
              )),
        ));
  }
}

เห็น Code แล้ว อาจจะดูมันเยอะน่ากลัวๆ แต่ว่าจริงๆแล้วไม่ซับซ้อนอะไรมากมายนัก แต่ต้องดูภาพรวมของโปรเจ๊ค ไฟล์ transaction_db.dart เป็น Class ที่ทำหน้าที่ติดต่อกับ Database

ไฟล์ transaction_provider.dart เป็นไฟล์เบื้องหลังที่ติดต่อ Database และข้อมูลจากฟอร์มกรอกข้อมูลและฟอร์มสำหรับแสดงผล

ไฟล์ main.dart เป็นไฟล์ที่ใช้แสดงผลเมื่อเปิดใช้งานแอป โดยจะมี Stattfull widget ทำหน้าที่แสดลข้อมูลผ่าน ListView Widget โดยจะเปิดใช้งาน Provider และ Consumer และมีการเรียกใช้ initState เพื่อเรียกเปิดฐานข้อมูลมาแสดง

ไฟล์ transaction.dart เป็น Class ของ Transactions <Map List> โครงสร้างข้อมูลที่จะบันทึกเก็บและการนำมาแสดง

ไฟล์ form_screem.dart เป็นไฟล์สำหรับกรอกข้อมูล เมื่อเสร็จแล้วส่งข้อมูลผ่าน provider ในรูปแบบ List

ใน code บางส่วนมีการดัดแปลงต่างจากคลิปที่สอนเนื่องจาก Version ของ Flutter ใน Clip เป็น version 1.x ซึงปัจจุบัน (2022) เป็น Version 3.3 แล้วนั้น บางอย่างจะมีเรื่อง Null Safty มาเกี่ยวข้อง และมีการตรวจสอบ รูปแบบข้อมูลใากขึ้น ทำให้ Code เดิม ไม่สามารถทำงานได้ ใน Version ปัจจุบัน

Leave a Reply

Your email address will not be published. Required fields are marked *