Expense Apps

แอปบันทึกค่าใช้จ่าย เพิ่มรายการใหม่ แสดงรายการและเลื่อนดูรายการทั้งหมด

เพิ่มรายการใหม่ Add Transaction

จัดวางคอลัมจากบนลงล่างไม่เว้นช่องว่าง

main.dart
 
	Column(
		mainAxisAlignment: MainAxisAlignment.start,
		crossAxisAlignment: CrossAxisAlignment.stretch,
		...
	)
	

สร้าง TextFild() สำหรับบันทึกรายการค่าใช้จ่าย
จัดวาง TextField() ไว้ระหว่าง Chart กับ Transaction
วาง Card(child:) ต่อจาก Chart Container() เพื่อให้กำหนด margin ได้
เนื่องจากมี TextField() 2 วิดเจ็ต ต้องใช้ Column(children: []) ที่เป็น child ของ Card() มาสร้าง
เพิ่ม TextField() ในลิสต์ของ Column(children:[])
เพิ่มปุ่มกด ElevatedButton() ด้านล่างของ TextField()
main.dart

 
	Card(
		 elevation: 5,
		 margin: EdgeInsets.all(10),
		 child: Column(
		   crossAxisAlignment: CrossAxisAlignment.end,
		   children: [
			   TextField(
				 decoration: InputDecoration(labelText: 'Title'),
			   ),
			   TextField(
				 decoration: InputDecoration(labelText: 'Amount'),
			   ),
			   ElevatedButton(
				 onPressed: () {},
				 child: Text('Add Item'),
			   ),
			],
		 ),
	   ),
	

textfield


สร้างตัวแปรเก็บค่าอินพุท title และ amount ต่อจาก [Transaction]

main.dart
 
final List trans = [
	...
	]
final title = TextEditingController();
final amount = TextEditingController();

เพิ่มพารามิเตอร์ controller ของ TextField(controller:) เพื่อเก็บค่าที่พิมพ์ใน TextField()
กดปุ่ม Add Item จะเห็นข้อมูล title และ amount ใน debug console
เรายังไม่สามารถนำข้อมูลอินพุทไปอัพเดทหน้าจอได้ เพราะหน้าจอยังเป็น StatelessWidget
ต้องเปลี่ยนหน้าจอเป็น StatefullWidget เพื่อทำการอัพเดท Transaction

main.dart
 
Card(
     elevation: 5,
     margin: EdgeInsets.all(10),
     child: Column(
       crossAxisAlignment: CrossAxisAlignment.end,
       children: [
           TextField(
             decoration: InputDecoration(labelText: 'Title'),
             controller: title,
           ),
           TextField(
             decoration: InputDecoration(labelText: 'Amount'),
             controller: amount,
           ),
           ElevatedButton(
             onPressed: () {
               print(title.text);
               print(amount.text);
             },
             child: Text('Add Item'),
           ),
         ],
     ),
   ),

input


Add and Update Transaction with StatefullWidget

หลักการคือแยกวิดเจ็ตที่เปลี่ยนแปลงข้อมูลและต้องอัพเดทหน้าจอเช่น [Transaction] ไปไว้ใน StatefullWidget เพื่อให้เรียกใช้ setState((){}) ได้ ส่วนวิดเจ็ตที่ไม่เปลี่ยนเช่น AppBar ก็ไว้ใน StatelessWidget เหมือนเดิม
วิดเจ็ตใหม่ที่สร้างเพิ่มคือ TransactionList() ในไฟล์ list.dart
UserTransaction() ในไฟล์ user_trans.dart
NewTransaction() ในไฟล์ new_trans.dart

จัดวางไฟล์และโฟลเดอร์ใหม่ดังนี้

สร้างโฟลเดอร์ models

ย้ายไฟล์ trans.dart ไปไว้ในโฟลเดอร์ models

trans.dart
 
	import 'package:flutter/foundation.dart';

	class Transaction {
	  final String id;
	  final String title;
	  final double amount;
	  final DateTime dt;
	
	  Transaction(
		  {@required this.id,
		  @required this.title,
		  @required this.amount,
		  @required this.dt});
	}

สร้างโฟลเดอร์ widgets

สร้างไฟล์ list.dart ในโฟลเดอร์ widgets และสร้างวิดเจ็ต TransactionList() เพื่อเก็บและแสดงรายการค่าใช้จ่ายทั้งหมด ซึ่งควรจะเป็น StatefullWidget แต่เนื่องจากเราจะห่อหุ้ม [Transaction] ด้วย UserTransaction() ซึ่งจะเป็น StatefullWidget ให้สร้าง TransactionList() แบบ StatelessWidget โดยให้ย้าย Card() ที่แสดงรายการค่าใช้จ่ายทั้งหมด ใน main.dart มาไว้ใน return ของ build() ของ TransactionList()

list.dart
 
	import 'package:flutter/material.dart';
	import '../models/trans.dart';
	import 'package:intl/intl.dart';
	
	class TransactionList extends StatelessWidget {
	  final List transactions;
	  TransactionList(this.transactions);
	  
	  @override
	  Widget build(BuildContext context) {
		return Column(
		  children: transactions.map((tx) {
			return Card(
			  child: Row(
				children: [
				  Container(
					margin: EdgeInsets.symmetric(
					  vertical: 10,
					  horizontal: 15,
					),
					decoration: BoxDecoration(
						border: Border.all(
					  color: Colors.blue,
					  width: 2,
					)),
					padding: EdgeInsets.all(10),
					child: Text(
					  '${tx.amount} บาท',
					  style: TextStyle(
						  fontWeight: FontWeight.bold,
						  fontSize: 20,
						  color: Colors.purple),
					),
				  ),
				  Column(
					crossAxisAlignment: CrossAxisAlignment.start,
					children: [
					  Text(
						tx.title,
						style: TextStyle(
						  fontWeight: FontWeight.bold,
						  fontSize: 15,
						),
					  ),
					  Text(
						//tx.dt.toString(),
						DateFormat().format(tx.dt),
						style: TextStyle(color: Colors.blue),
					  ),
					],
				  ),
				],
			  ),
			);
		  }).toList(),
		);
	  }
	}	

สร้างไฟล์ new_trans.dart ในโฟลเดอร์ widgets และสร้างวิดเจ็ต NewTransaction() เพื่อรับอินพุทในการสร้างรายการค่าใช้จ่าย ด้วย TextField() โดยให้ย้าย Card() ที่แสดง TextField() และปุ่มกด ElevatedButton() ใน main.dart มาไว้ใน return ของ build() ของ NewTransaction()

new_trans.dart
 
	import 'package:flutter/material.dart';

	class NewTransaction extends StatelessWidget {
	  final Function addTrans;
	  final title = TextEditingController();
	  final amount = TextEditingController();
	
	  NewTransaction(this.addTrans);
	  @override
	  Widget build(BuildContext context) {
		return Card(
		  elevation: 5,
		  margin: EdgeInsets.all(10),
		  child: Column(
			crossAxisAlignment: CrossAxisAlignment.end,
			children: [
			  TextField(
				decoration: InputDecoration(labelText: 'Title'),
				controller: title,
			  ),
			  TextField(
				decoration: InputDecoration(labelText: 'Amount'),
				controller: amount,
			  ),
			  ElevatedButton(
				onPressed: () {
				  print(title.text);
				  print(amount.text);
				  addTrans(title.text,double.parse(amount.text));
				},
				child: Text('Add Item'),
			  ),
			],
		  ),
		);
	  }
	}	
<ิ>สร้างไฟล์ user_trans.dart ในโฟลเดอร์ widgets และสร้างวิดเจ็ต UserTransaction() แบบ StatefullWidget เพื่อเรียกใช้ NewTransaction() และ TransactionList() และให้อัพเดทหน้าจอเมื่อมีการสร้างรายการค่าใช้จ่ายอันใหม่
ให้ย้ายรายการ Transaction() ใน main.dart มาไว้ใน UserTransaction()
สร้างฟังก์ชัน _addNewTrans() เพื่อสร้างรายการใหม่และอัพเดทหน้าจอใน UserTransaction()
ส่งชื่อฟังก์ชันหรือพอยต์เตอร์ _addNewTrans ไปยัง NewTransaction(_addNewTrans) เพื่อให้สามารถเรียกใช้ _addNewTrans() ในการเพิ่มรายการใหม่ได้

user_trans.dart
 
	import 'package:flutter/material.dart';
	import './list.dart';
	import './new_trans.dart';
	import '../models/trans.dart';
	
	class UserTransaction extends StatefulWidget {
	  @override
	  _UserTransactionState createState() => _UserTransactionState();
	}
	
	class _UserTransactionState extends State {
	  final List _userTrans = [
		Transaction(
		  id: 't1',
		  title: 'bag',
		  amount: 299.9,
		  dt: DateTime.now(),
		),
		Transaction(
		  id: 't2',
		  title: 'paper',
		  amount: 99.9,
		  dt: DateTime.now(),
		),
	  ];
	  void _addNewTrans(String txTitle, double txAmount) {
		final newTrans = Transaction(
		  id: DateTime.now().toString(),
		  title: txTitle,
		  amount: txAmount,
		  dt: DateTime.now(),
		);
		setState(() {
		  _userTrans.add(newTrans);
		});
	  }
	
	  @override
	  Widget build(BuildContext context) {
		return Column(children: [
		  NewTransaction(_addNewTrans),
		  TransactionList(_userTrans),
		]);
	  }
	}	

เรียกใช้ UserTransaction() ใน main.dart

main.dart
 
	import 'package:expense/widgets/user_trans.dart';
	import 'package:flutter/material.dart';
	void main() => runApp(MyApp());
	
	class MyApp extends StatelessWidget {
	  // This widget is the root of your application.
	  @override
	  Widget build(BuildContext context) {
		return MaterialApp(
		  title: 'Expense App',
		  home: MyHomePage(),
		);
	  }
	}
	
	class MyHomePage extends StatelessWidget {
	  @override
	  Widget build(BuildContext context) {
		return Scaffold(
		  appBar: AppBar(
			title: Text('Expense App'),
		  ),
		  body: Column(
			//mainAxisAlignment: MainAxisAlignment.start,
			crossAxisAlignment: CrossAxisAlignment.stretch,
			children: [
			  Container(
				width: double.infinity,
				child: Card(
				  color: Colors.blue,
				  child: Text('Chart'),
				  elevation: 5,
				),
			  ),
			  UserTransaction()
			],
		  ), // This trailing comma makes auto-formatting nicer for build methods.
		);
	  }
	}	

run app, add new item

เมื่อเพิ่มรายการไปเรื่อยๆ จะมีปัญหาในการแสดงผลรายการด้านล่าง ยังเลื่อนดูไม่ได้ และมีข้อผิดพลาดในการแสดงผล ที่ด้านล่างแรเงาสีเหลืองดำ

newtran1


newtran2


newtran3


ทำหน้าจอให้เลื่อนขึ้นลงได้ ทั้งหน้าจอ

เมื่อเราเพิ่มรายการจนเต็มหน้าจอ ก็จะมีปัญหาในการแสดงผล Column() รายการค่าใช้จ่าย เพราะมันเกินพื้นที่หน้าจอ
วิธีการแก้คือต้องทำให้ Column() สามารถเลื่อไปมาบนหน้าจอได้
ให้ทำการหุ้ม Column() ด้วยวิดเจ็ต SingleChildScrollView() ซึ่งเป็นวิเจ็ตที่เลื่อนหน้าจอขึ้นลงได้
ไปที่ main.dart กดขวาที่ Column() ของ body: Column() แล้วเลือก Refactor... เลือก Wrap with widget... เปลี่ยนชื่อวิดเจ็ตเป็น SingleChildScrollView

main.dart
 
	class MyHomePage extends StatelessWidget {
		@override
		Widget build(BuildContext context) {
		  return Scaffold(
			appBar: AppBar(
			  title: Text('Expense App'),
			),
			body: SingleChildScrollView(
			  child: Column(
				//mainAxisAlignment: MainAxisAlignment.start,
				crossAxisAlignment: CrossAxisAlignment.stretch,
				children: [
				  Container(
					width: double.infinity,
					child: Card(
					  color: Colors.blue,
					  child: Text('Chart'),
					  elevation: 5,
					),
				  ),
				  UserTransaction()
				],
			  ),
			), // This trailing comma makes auto-formatting nicer for build methods.
		  );
		}
	  }

scroll


ทำหน้าจอให้เลื่อนขึ้นลงได้ บางส่วนของหน้าจอ

สังเกตว่าการใช้ SingleChildScrollView() ใน main.dart ทำให้เลื่อนหน้าจอขึ้นลงได้ทั้งส่วนที่รับอินพุท NewTransaction() และส่วนที่แสดงรายการ TransactionList() ถ้าเราต้องการให้เลื่อนหน้าจอเฉพาะส่วนที่แสดงรายการ เราต้องใช้ SingleChildScrollView() ใน TransactionList() ด้วย โดยเราตั้งกำหนดความสูงของหน้าจอส่วนที่เลื่อนขึ้นลง
แต่ในคลาส TransactionList() ฟังก์ชัน build() รีเทิน Column() ซึ่งกำหนดความสูงไม่ได้ ก็ให้เราหุ้ม Column() ด้วย Container()
ไปที่ list.dart ในฟังก์ชัน build() ที่ return ให้กดขวาที่ Column() เลือก Refactor... เลือก Wrap with Container
กำหนดความสูงให้กับ Container(height: 400) ตัวเลขความสูงควรปรับให้พอดีกับพื้นที่ใช้งานของ TransactionList()
จากนั้นก็ให้หุ้ม Column() ใน Container() ด้วย SingleChildScrollView()
เพิ่มรายการใหม่หลายๆรายการ กดเลื่อนดูรายการส่วน TransactionList() ขึ้นลงได้ โดยที่ส่วนรับอินพุท NewTransaction() จะอยู่กับที่ ยกเว้นเรากำหนดความสูงของ TransactionList() เกินขนาดหน้าจอเช่น height: 500 หรือเราหมุนหน้าจอในแนวนอน ถ้าพื้นที่หน้าจอหลักไม่เพียงพอสำหรับแสดงรายการหรือแสดงคีย์บอร์ด ฟลัตเตอร์ก็จะเลื่อนส่วนที่รับอินพุทขึ้นไปเพื่อให้แสดงรายการหรือคีย์บอร์ดได้

list.dart
 
	import 'package:flutter/material.dart';
	import '../models/trans.dart';
	import 'package:intl/intl.dart';
	
	class TransactionList extends StatelessWidget {
	  final List transactions;
	  TransactionList(this.transactions);
	  
	  @override
	  Widget build(BuildContext context) {
		return Container(
		  height: 400,
		  child: SingleChildScrollView(
			child: Column(
			  children: transactions.map((tx) {
				return Card(
				  child: Row(
					children: [
					  Container(
						margin: EdgeInsets.symmetric(
						  vertical: 10,
						  horizontal: 15,
						),
						decoration: BoxDecoration(
							border: Border.all(
						  color: Colors.blue,
						  width: 2,
						)),
						padding: EdgeInsets.all(10),
						child: Text(
						  '${tx.amount} บาท',
						  style: TextStyle(
							  fontWeight: FontWeight.bold,
							  fontSize: 20,
							  color: Colors.purple),
						),
					  ),
					  Column(
						crossAxisAlignment: CrossAxisAlignment.start,
						children: [
						  Text(
							tx.title,
							style: TextStyle(
							  fontWeight: FontWeight.bold,
							  fontSize: 15,
							),
						  ),
						  Text(
							//tx.dt.toString(),
							DateFormat().format(tx.dt),
							style: TextStyle(color: Colors.blue),
						  ),
						],
					  ),
					],
				  ),
				);
			  }).toList(),
			),
		  ),
		);
	  }
	}	

scroll2


ListView() วิดเจ็ต

อีกวิธีที่ดีกว่าในการเลื่อนหน้าจอเฉพาะส่วนที่แสดงรายการ คือการใช้ ListView() ใน TransactionList() แทนที่ SingleChildScrollView() และ Coulumn() โดยเราต้องกำหนดความสูงของหน้าจอส่วนที่เลื่อนขึ้นลงภายใน Container() เช่นเดิม
ในคลาส TransactionList() ฟังก์ชัน build() ให้เอา SingleChildScrollView() ออก และแทนที่ Column() ด้วย ListView()
รันแอป จะได้ผลลัพธ์เหมือนกับการใช้ SingleChildScrollView()

list.dart
 
	@override
	Widget build(BuildContext context) {
	  return Container(
		height: 400,
		child: ListView (
		  children: transactions.map((tx) {
			return Card(
			  child: Row(
				children: [
				  Container(
					margin: EdgeInsets.symmetric(
					  vertical: 10,
					  horizontal: 15,
					),
					decoration: BoxDecoration(
						border: Border.all(
					  color: Colors.blue,
					  width: 2,
					)),
					padding: EdgeInsets.all(10),
					child: Text(
					  '${tx.amount} บาท',
					  style: TextStyle(
						  fontWeight: FontWeight.bold,
						  fontSize: 20,
						  color: Colors.purple),
					),
				  ),
				  Column(
					crossAxisAlignment: CrossAxisAlignment.start,
					children: [
					  Text(
						tx.title,
						style: TextStyle(
						  fontWeight: FontWeight.bold,
						  fontSize: 15,
						),
					  ),
					  Text(
						//tx.dt.toString(),
						DateFormat().format(tx.dt),
						style: TextStyle(color: Colors.blue),
					  ),
					],
				  ),
				],
			  ),
			);
		  }).toList(),
		),
	  );
	}

listview


ListlView() and ListlView.builder()

ถ้ามีรายการจำนวนมาก ListlView() จะประมวลผลรายการทั้งหมดไม่ว่ารายการนั้นจะอยู่นอกส่วนที่มองเห็นบนหน้าจอ
ถ้าจะให้ประมวลผลและแสดงผลเฉพาะรายการที่มองเห็นบนหน้าจอเท่านั้น ต้องเปลี่ยนไปใช้ ListlView.builder()

listviewbuilder


แสดงผลรายการด้วย ListlView.builder()

ในคลาส TransactionList() แทนที่ ListlView() ด้วย ListlView.builder()
เพิ่มพารามิเตอร์ของ itemBuilder:(BuildContext, int){} ซึ่งเป็นฟังก์ชันที่มีสองพารามิเตอร์ ใน ListlView.builder()
และต้องเพิ่ม พารามิเตอร์ itemCount: เพื่อบอก ListlView.builder() ว่ามีกี่รายการที่ต้องประมวลผล ซึ่งจำวนรายการก็คือ transactions.length นั่นเอง
เนื่องจาก ListlView.builder() ประมวลผลรายการจากจำนวน itemCount ก็ไม่จำเป็นต้องใช้ map() เพื่อดึงแต่ละรายการออกมา
ในส่วน { } ของ itemBuilder เราต้องรีเทิน Card() ที่แสดงผลแต่ละรายการให้กับ itemBuilder: ของ ListView.builder()
ให้คัดลอก Card() ส่วนที่แสดงผลรายการ มาวางไว้ต่อจาก return ของ (BuildContext, int){ }
แทนที่พารามิเตอร์ tx ของ map() ด้วย transactions[int]
ลบคำสั่งที่เรียกใช้ map() คือ children: transactions.map((tx) ออกไป

list.dart
 
	import 'package:flutter/material.dart';
	import '../models/trans.dart';
	import 'package:intl/intl.dart';
	
	class TransactionList extends StatelessWidget {
	  final List transactions;
	  TransactionList(this.transactions);
	
	  @override
	  Widget build(BuildContext context) {
		return Container(
		  height: 400,
		  child: ListView.builder(
			itemCount: transactions.length,
			itemBuilder: (context, index) {
			  return Card(
				child: Row(
				  children: [
					Container(
					  margin: EdgeInsets.symmetric(
						vertical: 10,
						horizontal: 15,
					  ),
					  decoration: BoxDecoration(
						  border: Border.all(
						color: Colors.blue,
						width: 2,
					  )),
					  padding: EdgeInsets.all(10),
					  child: Text(
						'${transactions[index].amount} บาท',
						style: TextStyle(
							fontWeight: FontWeight.bold,
							fontSize: 20,
							color: Colors.purple),
					  ),
					),
					Column(
					  crossAxisAlignment: CrossAxisAlignment.start,
					  children: [
						Text(
						  transactions[index].title,
						  style: TextStyle(
							fontWeight: FontWeight.bold,
							fontSize: 15,
						  ),
						),
						Text(
						  //tx.dt.toString(),
						  DateFormat('dd MMM yyyy').format(transactions[index].dt),
						  style: TextStyle(color: Colors.blue),
						),
					  ],
					),
				  ],
				),
			  );
			},
		  ),
		);
	  }
	}

run app ได้ผลลัพธ์เหมือนกัน

listviewbuilder2

2020. mnet50.com