Professional Documents
Culture Documents
Flutter Cơ Bản
Flutter Cơ Bản
1
Lấy từ https://vncoder.vn/bai-hoc/gioi-thieu-flutter-204, có chỉnh sửa nhỏ thêm.
Bài 9: Quản lý trạng thái State trong Flutter 41
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(this.title),
),
body: Center(
child:
Text(
'Hello World',
)
),
);
}
}
Mình sẽ giải thích ý nghĩa chi tiết của đoạn code trên
● Dòng 1 − import flutter package, tên là material. Material là một flutter package được sử dụng để
tạo giao diện người dùng theo Material design cho Android.
● Dòng 2 − là điểm khởi đầu của Flutter application là hàm main của ứng dụng. Phương thức
runApp được gọi và truyền vào đối tượng của lớp MyApp. Mục đích chính của phương thức
runApp là đưa giao diện widget vào hiển thị trên màn hình
● Dòng 5-17 − Widget được sử dụng để tạo UI (giao diện người dùng) qua flutter framework.
StatelessWidget là một widget, nó không bao gồm trạng thái nào của widget. MyApp kế thừa
StatelessWidget và ghi đè phương thức build method. Mục đích của phương thức build là tạo một
phần UI cho ứng dụng. Ở đây, phương thức build sử dụng MaterialApp, một widget để tạo layout
UI gốc cho ứng dụng. Bao gồm 3 thành phần chính là - title (tiêu đề), theme (chủ đề) và home
(trang chủ hay phần màn hình).title là tiêu đề của ứng dụng theme là chủ đề của widget. Ở đây,
chúng ta set chủ đề là blue đó là màu sắc chủ đạo của ứng dụng thông qua class ThemeData và
các thuộc tính của nó ví dụ primarySwatch.home phần màn hình của ứng dụng, nó được tạo bởi
một widget khác, MyHomePage
● Dòng 19 - 38 − MyHomePage tương tự MyApp ngoại trừ nó trả về Scaffold Widget. Scaffold là một
top level widget đứng sau MaterialApp widget được sử dụng để tạo UI theo material design. Nó
có hai thuộc tính quan trọng nhất là appBar để hiển thị phần đầu của ứng dụng và body để hiển thị
nội dung chính của ứng dụng. AppBar là một widget khác để tạo phần đầu của ứng dụng và sử
dụng các thuộc tính của appBar. Trong các thuộc tính của body, chúng ta sử dụng một Center
widget, đây là một widget con. Text là một widget cuối cùng, phổ biến để hiển thị một văn bản
giữa màn hình
Bước 8 − Bây giờ chúng ta bắt đầu chạy ứng dụng bằng lệnh Run → Run main.dart
Lưu ý bạn cần kết nối với một thiết bị Android thật thông qua cab USB (bật chế độ nhà phát triển) hoặc kết
nối với một máy ảo Android Emulator
Trong trường hợp Android Studio báo lỗi không kết nối được với thiết bị (flutter run: No connected
devices) có thể là do bạn chưa chọn API Android cho project
Các bạn chọn File → Project Structure và thêm vào SDK Android mới nhất
Kết quả khi chạy Project Flutter trên máy ảo Android Emulator.
Như vậy là mình đã hướng dẫn các bạn tạo ứng dụng Flutter đầu tiên, giải thích các thành phần chính của
project cũng như code của file main. Trong bài tiếp theo mình sẽ hướng dẫn các bạn tìm hiểu kiến trúc
của ứng dụng Flutter
Layers
Một khái niệm quan trọng của Flutter framework đó là các thành phần sẽ được nhóm lại theo độ phức tạp
và được sắp xếp rõ ràng trong các tầng có độ phức tạp giảm dần. Một layer (tầng,lớp) được tạo thành
bằng việc sử dụng các class tiếp theo ngay canh nó. Top của tất cả các layer là các widget đặc biệt cho
Android và iOS. Layer tiếp theo là widget gốc của flutter. Tiếp nữa là Rendering layer, đây là level thấp nhất
trong việc sinh các thành phần của flutter app. Layer tiếp theo là nền tảng gốc hệ điều hành.
Tổng quan về layer của Flutter được mô tả trong sơ đồ dưới đây:
Tổng kết
Tổng kết những điểm chính về kiến trúc của Flutter:
● Trong Flutter, tất cả đều quy về các widget, một widget phức hợp sẽ bao gồm các widget khác
bên trong
● Các tính năng tương tác sẽ được tích hợp bất cứ khi nào nhờ GestureDetector widget.
● Trạng thái của các widget được quản lý cập nhật bởi StatefulWidget widget.
● Flutter cung cấp thiết kế class để bất kỳ lớp nào có thể được lập trình tùy thuộc vào độ phức tạp
của tác vụ.
Hàm (Functions)
Hàm là một nhóm các câu lệnh nhằm thực hiện một tác vụ nhất định. Chúng ta cùng xem một ví dụ về
hàm trong Dart dưới đây:
void main() {
add(3,4);
}
void add(int a,int b) {
int c;
c = a+b;
print(c);
}
Hàm trên làm nhiệm vụ cộng hai tham số truyền vào, và in ra kết quả trên màn hình
//getter method
String get emp_name {
return name;
}
//setter method
void set emp_name(String name) {
this.name = name;
}
//function definition
void result() {
print(name);
}
}
void main() {
//object creation
Employee emp = new Employee();
emp.name = "employee1";
emp.result(); //function call
}
Ở bài tiếp theo chúng ta sẽ tìm hiểu kĩ hơn về widget - thành phần cơ bản nhất của Flutter
Layout widgets
Trong Flutter, một widget có thể được tạo thành từ một hoặc nhiều widget khác. Việc kết hợp nhiều
widget thành một widget được thực hiện thông qua các layout widget. Ví dụ, các widget con có thể được
căn giữa thông qua Center widget.
Một số layout widgets phổ biến:
● Container − Một hình chữ nhật được thiết kế sử dụng BoxDecoration widgets với background
(nền), border (đường viền) và shadow (bóng đổ).
● Center − Căn giữa các widget con.
● Row − Sắp xếp các widget con theo hàng ngang (horizontal direction).
● Column − Sắp xếp các widget con theo hàng dọc (vertical direction).
● Stack − Sắp xếp các widget con lên trên cùng
Chúng ta sẽ tìm hiểu kỹ hơn trong bài Giới thiệu layout widget ở các bài sau.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar( title: Text(this.title), ),
body: Center( child: Image.asset("assets/smiley.png")),
);
}
}
Chạy thử ứng dụng hình ảnh sẽ được load lên như sau:
Một số thuộc tính cơ bản của Image widget:
● image, ImageProvider − Kích thước ảnh
● width, double − Độ rộng của ảnh
● height, double − Độ cao của ảnh
● alignment, AlignmentGeometry − Căn lề
Icon
Icon widget hiển thị hình ảnh các icon cơ bản trong IconData class.
Icon(Icons.email)
Chúng ta sửa lại code MyHomePage widget như sau:
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: Center( child: Icon(Icons.email)),
);
}
}
Chạy thử trên máy ảo
Bài viết khá dài, mình sẽ hướng dẫn các bạn chi tiết cách sử dụng các widget chính để thiết kế giao diện ở
những bài cuối nhé, trước khi tìm hiểu kỹ các thành phần của Flutter
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(width: 1.0, color: Color(0xFFFFFFFFFF)),
left: BorderSide(width: 1.0, color: Color(0xFFFFFFFFFF)),
right: BorderSide(width: 1.0, color: Color(0xFFFF000000)),
bottom: BorderSide(width: 1.0, color: Color(0xFFFF000000)),
),
),
child: Container(
padding: const
EdgeInsets.symmetric(horizontal: 20.0, vertical: 2.0),
decoration: const BoxDecoration(
border: Border(
top: BorderSide(width: 1.0, color: Color(0xFFFFDFDFDF)),
left: BorderSide(width: 1.0, color: Color(0xFFFFDFDFDF)),
right: BorderSide(width: 1.0, color: Color(0xFFFF7F7F7F)),
bottom: BorderSide(width: 1.0, color: Color(0xFFFF7F7F7F)),
),
color: Colors.grey,
),
child: const Text(
'OK',textAlign: TextAlign.center, style: TextStyle(color: Colors.black)
),
),
);
}
}
Đoạn code trên sử dụng 2 widget một Container widget và một Text widget. Kết quả như sau:
Ví dụ
Để hiểu hơn về layout trong Flutter mình sẽ hướng dẫn các bạn thực hiện một giao diện người dùng phức
hợp gọi là product listing với thiết kế tùy chỉnh sử dụng cả single và multiple child layout widget
Các bạn tạo project Flutter mới đặt tên là product_layout_app.
Thay thế main.dart bằng đoạn code sau đây:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(this.title),),
body: Center(child: Text( 'Hello World', )),
);
}
}
Chúng ta tạo một widget là MyHomePage kế thừa từ StatelessWidget
Tiếp theo chúng ta sẽ tạo một widget ProductBox để hiển thị thông tin sản phẩm, bao gồm hình ảnh, tên
sản phẩm, mô tả, và giá bán như thiết kế dưới đây:
Bây giờ, chúng ta thêm ảnh của sản phẩm vào thư mục assets của ứng dụng, tạo thư mục con appimages
để chứa ảnh và cấu hình assets trong file pubspec.yaml như sau:
assets:
- assets/appimages/floppydisk.jpg
- assets/appimages/iphone.jpg
- assets/appimages/laptop.jpg
- assets/appimages/pendrive.jpg
- assets/appimages/pixel.jpg
- assets/appimages/tablet.jpg
Các bạn nhấn chuột phải vào ảnh rồi download về máy nhé.
Bây giờ để hiển thị nhiều sản phẩm ta sử dụng ListView widget để chứa các ProductBox, sửa lại code
MyHomePage như sau:
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("Product Listing")),
body: ListView(
shrinkWrap: true, padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0),
children: <Widget> [
ProductBox(
name: "iPhone",
description: "iPhone is the stylist phone ever",
price: 1000,
image: "iphone.jpg"
),
ProductBox(
name: "Pixel",
description: "Pixel is the most featureful phone ever",
price: 800,
image: "pixel.jpg"
),
ProductBox(
name: "Laptop",
description: "Laptop is most productive development tool",
price: 2000,
image: "laptop.jpg"
),
ProductBox(
name: "Tablet",
description: "Tablet is the most useful device ever for meeting",
price: 1500,
image: "tablet.jpg"
),
ProductBox(
name: "Pendrive",
description: "Pendrive is useful storage medium",
price: 100,
image: "pendrive.jpg"
),
ProductBox(
name: "Floppy Drive",
description: "Floppy drive is useful rescue storage medium",
price: 20,
image: "floppydisk.jpg"
),
],
)
);
}
}
Full code (main.dart) của ứng dụng (product_layout_app) như sau:
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: 'Flutter Demo', theme: ThemeData(
primarySwatch: Colors.blue,),
home: MyHomePage(title: 'Product layout demo home page'),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title:Text("Product Listing")),
body: ListView(
shrinkWrap: true, padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0),
children: <Widget> [
ProductBox(
name: "iPhone",
description: "iPhone is the stylist phone ever",
price: 1000,
image: "iphone.jpg"
),
ProductBox(
name: "Pixel",
description: "Pixel is the most featureful phone ever",
price: 800,
image: "pixel.jpg"
),
ProductBox(
name: "Laptop",
description: "Laptop is most productive development tool",
price: 2000,
image: "laptop.jpg"
),
ProductBox(
name: "Tablet",
description: "Tablet is the most useful device ever for meeting",
price: 1500,
image: "tablet.jpg"
),
ProductBox(
name: "Pendrive",
description: "Pendrive is useful storage medium",
price: 100,
image: "pendrive.jpg"
),
ProductBox(
name: "Floppy Drive",
description: "Floppy drive is useful rescue storage medium",
price: 20,
image: "floppydisk.jpg"
),
],
)
);
}
}
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.name, this.description, this.price, this.image})
: super(key: key);
final String name;
final String description;
final int price;
final String image;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(this.title),),
body: Center(
child: GestureDetector(
onTap: () {
_showDialog(context);
},
child: Text( 'Hello World', )
)
),
);
}
}
Chạy thử trên máy ảo, chạm vào chữ Hello Word sẽ có một cửa sổ dialog hiện ra như sau:
Cuối cùng, Flutter cũng cung cấp một cơ chế phát hiện cử chỉ cấp thấp thông qua Listener widget. Nó sẽ
phát hiện tất cả các tương tác của người dùng và sau đó gửi các sự kiện sau:
● PointerDownEvent
● PointerMoveEvent
● PointerUpEvent
● PointerCancelEvent
Flutter còn cung cấp một số nhỏ các widget để thực hiện các cử chỉ cụ thể đơn giản cũng như phức tạp:
● Dismissible − Hỗ trợ flick gesture để đóng widget.
● Draggable − Hỗ trợ drag gesture để di chuyển widget.
● LongPressDraggable − Hỗ trợ drag gesture để duy chuyển widget, khi widget cha có thể kéo thả
● DragTarget − Chấp nhận Draggable widget
● IgnorePointer − Ẩn widget
● AbsorbPointer − Dừng việc xử lý cử chỉ trên widget
● Scrollable − Hỗ trợ cuộn nội dung trong widget
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 1 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
iconSize: _size,
),
), Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 2 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
iconSize: _size,
),
), Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 3 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
iconSize: _size,
),
),
],
);
}
Ở đây, chúng ta sử dụng 3 ngôi sao, được tạo nên từ IconButton widget và sắp xếp sử dụng Row widget
trên cùng 1 hàng ngang. Ý tưởng ở đây là hiển thị rating thông qua dãy các ngôi sao. Ví dụ nếu rating của
sản phẩm là 2 sao thì hai ngôi sao đầu sẽ có màu đỏ và ngôi sao cuối cùng sẽ có màu trắng.
Thêm phương thức thay đổi trạng thái cho _RatingBoxState
void _setRatingAsOne() {
setState( () {
_rating = 1;
});
}
void _setRatingAsTwo() {
setState( () {
_rating = 2;
});
}
void _setRatingAsThree() {
setState( () {
_rating = 3;
});
}
Thay đổi trạng thái của rating theo cử chỉ của người dùng (chạm vào ngôi sao)
Widget build(BuildContext context) {
double _size = 20;
print(_rating);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 1 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
onPressed: _setRatingAsOne,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 2 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
onPressed: _setRatingAsTwo,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 3 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
onPressed: _setRatingAsThree,
iconSize: _size,
),
),
],
);
}
Ở đây sự kiện onPressed sẽ gọi hàm tương ứng để thay đổi trạng thái của widget và hiển thị ra giao diện.
Khi trạng thái bị thay đổi, hàm buid sẽ được gọi lại và giao diện sẽ được hiển thị lại.
Toàn bộ code của RatingBox widget như sau:
class RatingBox extends StatefulWidget {
@override
_RatingBoxState createState() => _RatingBoxState();
}
class _RatingBoxState extends State<RatingBox> {
int _rating = 0;
void _setRatingAsOne() {
setState( () {
_rating = 1;
});
}
void _setRatingAsTwo() {
setState( () {
_rating = 2;
});
}
void _setRatingAsThree() {
setState( () {
_rating = 3;
});
}
Widget build(BuildContext context) {
double _size = 20;
print(_rating);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 1 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
onPressed: _setRatingAsOne,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 2 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
onPressed: _setRatingAsTwo,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 3 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
onPressed: _setRatingAsThree,
iconSize: _size,
),
),
],
);
}
}
Bây giờ ta sẽ chỉnh sửa lại ứng dụng product_layout_app ở bài trước bằng cách bổ sung RatingBox widget
và thêm vào ProductBox như sau:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title:Text("Product Listing")),
body: ListView(
shrinkWrap: true, padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0),
children: <Widget> [
ProductBox(
name: "iPhone",
description: "iPhone is the stylist phone ever",
price: 1000,
image: "iphone.jpg"
),
ProductBox(
name: "Pixel",
description: "Pixel is the most featureful phone ever",
price: 800,
image: "pixel.jpg"
),
ProductBox(
name: "Laptop",
description: "Laptop is most productive development tool",
price: 2000,
image: "laptop.jpg"
),
ProductBox(
name: "Tablet",
description: "Tablet is the most useful device ever for meeting",
price: 1500,
image: "tablet.jpg"
),
ProductBox(
name: "Pendrive",
description: "Pendrive is useful storage medium",
price: 100,
image: "pendrive.jpg"
),
ProductBox(
name: "Floppy Drive",
description: "Floppy drive is useful rescue storage medium",
price: 20,
image: "floppydisk.jpg"
),
],
)
);
}
}
class RatingBox extends StatefulWidget {
@override
_RatingBoxState createState() =>
_RatingBoxState();
}
class _RatingBoxState extends State<RatingBox> {
int _rating = 0;
void _setRatingAsOne() {
setState( () {
_rating = 1;
});
}
void _setRatingAsTwo() {
setState( () {
_rating = 2;
});
}
void _setRatingAsThree() {
setState( () {
_rating = 3;
});
}
Widget build(BuildContext context) {
double _size = 20;
print(_rating);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 1 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
onPressed: _setRatingAsOne,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 2 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
onPressed: _setRatingAsTwo,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 3 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
onPressed: _setRatingAsThree,
iconSize: _size,
),
),
],
);
}
}
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.name, this.description, this.price, this.image}) :
super(key: key);
final String name;
final String description;
final int price;
final String image;
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(2),
height: 140,
child: Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Image.asset("assets/appimages/" + image),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(this.name, style: TextStyle(fontWeight:
FontWeight.bold)),
Text(this.description),
Text("Price: " + this.price.toString()),
RatingBox(),
],
)
)
)
]
)
)
);
}
}
Các bạn chạy thử ứng dụng, và chạm vào các ngôi sao để xem trạng thái của RatingBox thay đổi
Như vậy trong bài này mình đã hướng dẫn các bạn làm quen với việc quản lý trạng thái của widget thông
qua StatefulWidget. Chúc các bạn học tốt
ScopedModel
ScopedModel là một widget, chúng ta hiểu đơn giản nó là một tiện ích để chúng ta có thể dễ dàng chuyển
Data Model từ widget cha xuống các widget con, cháu của nó. Ngoài ra nó còn có nhiệm vụ rebuild lại các
widget con giữ các model mà trong trường hợp model này được cập nhật. Nếu cần nhiều hơn một Data
Model thì chúng ta có thể sử dụng lồng ScopeModel. Dưới đây là hai dạng ScopedModel :
Single model :
ScopedModel<Product>(
model: item, child: AnyWidget()
)
Multiple model
ScopedModel<Product>(
model: item1,
child: ScopedModel<Product>(
model: item2, child: AnyWidget(),
),
)
ScopeModel.of là một phương thức dùng để lấy Data Model dưới ScopeModel. Và nó có thể được sử
dụng khi Data Model thay đổi kể cả khi giao diện (UI) không thay đổi. Dưới đây là ví dụ khi ta thay đổi UI(
đánh giá ) của một sản phẩm
ScopedModel.of<Product>(context).updateRating(2);
ScopedModelDescendant
ScopedModelDescendant là một widget, nó lấy Data Model từ lớp cha và build lại UI bất kí khi nào Data
Model thay đổi.
ScopedModelDescendant có 2 thuộc tính là builder và child. Child là phần UI không bị thay đổi và sẽ được
chuyển cho hàm builder. Hàm buider sẽ nhận 3 đối số:
1. Content :ScopedModelDescendant chuyển sang context của ứng dụng
2. Child : Một phần của UI và không thay đổi dựa trên Data Model
3. Model : Dưới đây là ví dụ tường minh
return ScopedModelDescendant<ProductModel>(
builder: (context, child, cart) => { ... Actual UI ... },
child: PartOfTheUI(),
);
Bây giờ chúng ta sẽ sử dụng các ví dụ từ các bài học trước và sử dụng ScopeModel thay vì
StatefulWidget:
1. Tạo một ứng dụng Flutter mới với tên project tùy ý bạn, ở đây tôi sẽ sử dụng tên là
product_scoped_model_app
2. Sau đó thay thế các dòng code mặc định trong hàm main.dart bằng product_state_app code nhé
3. Coppy các hình ảnh từ assets mà đã sử dụng trong các bài trước, tôi sẽ để link lại đây nhé (
https://vncoder.vn/bai-hoc/layout-trong-flutter-225 ) sau đó vào pubspec.yaml file tìm đến mục
assets và dán vào
flutter:
assets:
- assets/floppy.jpg
- assets/iphone.jpg
- assets/laptop.jpg
- assets/pendrive.jpg
- assets/pixel.jpg
- assets/tablet.jpg
Bây giờ chúng ta phải sử dụng gói thư viện của bên thứ ba vì nó không có trong Framework của flutter
Bằng cách thêm Scope_model vào pubspec.yaml ởphần dependencies
dependencies: scoped_model: ^1.0.1
Oke, bạn nên sử dụng version mới nhất nhé, cập nhật tại đây : https://pub.dev/packages/scoped_model
Bây giờ bạn hãy thay thế đoạn code mặc định(main.dart) bằng đoạn code của chúng tôi nhé
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(this.title),
),
body: Center(
child: Text( 'Hello World', )
),
);
}
}
Bạn nhớ import Scope_model vào main.dart nhé.
import 'package:scoped_model/scoped_model.dart';
Giờ chúng ta sẽ tạo lớp Product quen thuộc, Product.dart là lớp chứa thông tin của sản phẩm Product
import 'package:scoped_model/scoped_model.dart';
class Product extends Model {
final String name;
final String description;
final int price;
final String image;
int rating;
items.add(
Product(
"Pixel",
"Pixel is the most feature-full phone ever", 800,
"pixel.jpg", 0
)
);
items.add(
Product(
"Laptop", "Laptop is most productive development tool", 2000,
"laptop.jpg", 0
)
);
items.add(
Product(
"Tablet",
"Tablet is the most useful device ever for meeting", 1500,
"tablet.jpg", 0
)
);
items.add(
Product(
"Pendrive",
"Pendrive is useful storage medium",
100, "pendrive.jpg", 0
)
);
items.add(
Product(
"Floppy Drive",
"Floppy drive is useful rescue storage medium", 20,
"floppy.jpg", 0
)
);
return items;
}
OK, tiếp tục chúng ta sẽ tạo một widget mới có tên là RatingBox và sử dụng Scope_model để support nhé
class RatingBox extends StatelessWidget {
RatingBox({Key key, this.item}) : super(key: key);
final Product item;
Ở đây chúng ta sử dụng StatelessWidget thay vì StatefulWidget. Cũng như vậy, chúng ta đã sử dụng
phương thức Product model’s updateRating để set giá trị phần đánh giá.
tiếp theo chúng ta sẽ điều chỉnh widget ProductBox để làm việc với lớp Product, ScopedModel và
ScopedModelDescendant.
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.item}) : super(key: key);
final Product item;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Product Navigation")),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ProductBox(item: items[index]);
},
)
);
}
}
Ở đây chúng ta đã sử dụng ListView.builder để xây dựng động danh sách Product
Vậy là xong, dưới đây là toàn bộ code được sử dụng :
Product.dart
import 'package:scoped_model/scoped_model.dart';
class Product extends Model {
final String name;
final String description;
final int price;
final String image;
int rating;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Product state demo home page'),
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
final items = Product.getProducts();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Product Navigation")),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ProductBox(item: items[index]);
},
)
);
}
}
class RatingBox extends StatelessWidget {
RatingBox({Key key, this.item}) : super(key: key);
final Product item;
Widget build(BuildContext context) {
double _size = 20;
print(item.rating);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
item.rating >= 1? Icon( Icons.star, size: _size, )
: Icon( Icons.star_border, size: _size, )
),
color: Colors.red[500],
onPressed: () => this.item.updateRating(1),
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (item.rating >= 2
? Icon(
Icons.star,
size: _size,
)
: Icon(
Icons.star_border,
size: _size,
)
),
color: Colors.red[500],
onPressed: () => this.item.updateRating(2),
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
item.rating >= 3 ?
Icon( Icons.star, size: _size, )
: Icon( Icons.star_border, size: _size, )
),
color: Colors.red[500],
onPressed: () => this.item.updateRating(3),
iconSize: _size,
),
),
],
);
}
}
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.item}) : super(key: key);
final Product item;
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(2),
height: 140,
child: Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Image.asset("assets/" + this.item.image),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: ScopedModel<Product>(
model: this.item, child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(
this.item.name, style: TextStyle(
fontWeight: FontWeight.bold
)
),
Text(this.item.description),
Text("Price: " + this.item.price.toString()),
ScopedModelDescendant<Product>(
builder: (context, child, item) {
return RatingBox(item: item);
}
)
],
)
)
)
)
]
),
)
);
}
}
Các bạn hãy thử chạy và xem kết quả. Nó cơ bản hoạt động giống ví dụ trước nhưng ở đây có sử dụng
thêm khái niệm Scope_model.
Scope_model - chúng ta có thể hiểu đơn giản nó là một framework, ví dụ khi bạn code "thô", lúc data model
thay đổi, ở hàm setState() thay vì rebuild lại tất cả các widget (Btn, txt, ..) thì sẽ phạm phải quy tắc Single
Responsibility thì Scope_model được sinh ra với mục đích chỉ rebuild data model bị thay đổi thay vì rebuild
tất cả widget
MaterialPageRoute
Đây là một widget được sử dụng để render giao diện người dùng nhằm thay thế toàn bộ màn hình với một
hiệu ứng chuyển đặc biệt nào đó
Tại đây, hàm buider sẽ chấp nhận chức năng để xây dựng nội dung bằng cách thay thế context hiện tại
của ứng dụng
MaterialPageRoute(builder: (context) => Widget())
Navigation.push
Từ một màn hình bất kì, ta muốn chuyển sang một màn hình khác sử dụng MaterialPageRoute widget
như sau :
Navigator.push( context, MaterialPageRoute(builder: (context) => Widget()), );
Navigation.pop
Được sử dụng để quay về trang trước, cách sử dụng đơn giản như sau :
Navigator.pop(context);
Để hiểu sâu hơn về Navigator chúng ta sẽ bắt tay vào xây dựng một ứng dụng đơn giản;. Let get started !!!
Tạo một ứng dụng Flutte mới với tên bất kì bạn muốn.Hãy copy thư mục assets từ product_nav_app sang
product_state_app và thêm assets vào file pubspec.yaml.( Hoặc các bạn có thể qua bài học này để lấy
hình ảnh nhé https://vncoder.vn/bai-hoc/layout-trong-flutter-225)
assets:
- assets/floppydisk.jpg
- assets/iphone.jpg
- assets/laptop.jpg
- assets/pendrive.jpg
- assets/pixel.jpg
- assets/tablet.jpg
Thay thế hàm main.dart mặc định bằng hàm main dưới đây
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
items.add(
Product(
"Pixel",
"Pixel is the most feature-full phone ever", 800,
"pixel.png"
)
);
items.add(
Product(
"Laptop",
"Laptop is most productive development tool",
2000, "
laptop.png"
)
);
items.add(
Product(
"Tablet",
"Tablet is the most useful device ever for meeting",
1500,
"tablet.png"
)
);
items.add(
Product(
"Pendrive",
"Pendrive is useful storage medium",
100,
"pendrive.png"
)
);
items.add(
Product(
"Floppy Drive",
"Floppy drive is useful rescue storage medium",
20,
"floppy.png"
)
);
return items;
}
';
Bây giờ chúng ta sẽ tạo một widget mới có tên là RatingBox dùng để đánh giá sản phẩm.
class RatingBox extends StatefulWidget {
@override
_RatingBoxState createState() =>_RatingBoxState();
}
class _RatingBoxState extends State<RatingBox> {
int _rating = 0;
void _setRatingAsOne() {
setState(() {
_rating = 1;
});
}
void _setRatingAsTwo() {
setState(() {
_rating = 2;
});
}
void _setRatingAsThree() {
setState(() {
_rating = 3;
});
}
Widget build(BuildContext context) {
double _size = 20;
print(_rating);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 1?
Icon(
Icons.star,
size: _size,
)
: Icon(
Icons.star_border,
size: _size,
)
),
color: Colors.red[500],
onPressed: _setRatingAsOne,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 2?
Icon(
Icons.star,
size: _size,
)
: Icon(
Icons.star_border,
size: _size,
)
),
color: Colors.red[500],
onPressed: _setRatingAsTwo,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 3 ?
Icon(
Icons.star,
size: _size,
)
: Icon(
Icons.star_border,
size: _size,
)
),
color: Colors.red[500],
onPressed: _setRatingAsThree,
iconSize: _size,
),
),
],
);
}
}
Chúng ta sẽ tạo một ProductBox widget là một item trong list Product dùng để hiển thị thông tin và đánh
giá sản phẩm.
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.item}) : super(key: key);
final Product item;
@override
Widget build(BuildContext context) {
return Scaffold( appBar: AppBar(title: Text("Product Navigation")),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return GestureDetector(
child: ProductBox(item: items[index]),
onTap: () {
Navigator.push(
context, MaterialPageRoute(
builder: (context) => ProductPage(item: items[index]),
),
);
},
);
},
));
}
}
OKE, bây giờ là phần quan trọng trong bài hôm nay. Bây giờ chúng ta sẽ sử dụng MaterialPageRoute để
chuyển sang screen chi tiết sản phẩm nhé.
class ProductPage extends StatelessWidget {
ProductPage({Key key, this.item}) : super(key: key);
final Product item;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(this.item.name),
),
body: Center(
child: Container(
padding: EdgeInsets.all(0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.asset("assets/appimages/" + this.item.image),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(
this.item.name, style: TextStyle(
fontWeight: FontWeight.bold
)
),
Text(this.item.description),
Text("Price: " + this.item.price.toString()),
RatingBox(),
],
)
)
)
]
),
),
),
);
}
}
Và chúng ta đã hoàn thành, dưới đây là toàn bộ code trong ứng dụng.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class Product {
final String name;
final String description;
final int price;
final String image;
Product(this.name, this.description, this.price, this.image);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Product Navigation")),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return GestureDetector(
child: ProductBox(item: items[index]),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductPage(item: items[index]),
),
);
},
);
},
)
);
}
}
class ProductPage extends StatelessWidget {
ProductPage({Key key, this.item}) : super(key: key);
final Product item;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(this.item.name),
),
body: Center(
child: Container(
padding: EdgeInsets.all(0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.asset("assets/" + this.item.image,height: 100.0,width:
MediaQuery.of(context).size.width,),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(this.item.name, style: TextStyle(fontWeight:
FontWeight.bold)),
Text(this.item.description),
Text("Price: " + this.item.price.toString()),
RatingBox(),
],
)
)
)
]
),
),
),
);
}
}
class RatingBox extends StatefulWidget {
@override
_RatingBoxState createState() => _RatingBoxState();
}
class _RatingBoxState extends State<RatingBox> {
int _rating = 0;
void _setRatingAsOne() {
setState(() {
_rating = 1;
});
}
void _setRatingAsTwo() {
setState(() {
_rating = 2;
});
}
void _setRatingAsThree() {
setState(() {
_rating = 3;
});
}
Widget build(BuildContext context) {
double _size = 20;
print(_rating);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 1 ? Icon(
Icons.star,
size: _size,
)
: Icon(
Icons.star_border,
size: _size,
)
),
color: Colors.red[500],
onPressed: _setRatingAsOne,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 2 ?
Icon(
Icons.star,
size: _size,
)
: Icon(
Icons.star_border,
size: _size,
)
),
color: Colors.red[500],
onPressed: _setRatingAsTwo,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 3 ?
Icon(
Icons.star,
size: _size,
)
: Icon(
Icons.star_border,
size: _size,
)
),
color: Colors.red[500],
onPressed: _setRatingAsThree,
iconSize: _size,
),
),
],
);
}
}
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.item}) : super(key: key);
final Product item;
Giới thiệu:
Animation là quá trình thể hiện một loạt các hình ảnh trong một khoảng thời gian. Một vài điều quan trọng
về Animation như sau :
1. Animation có 2 giá trị đích : đầu và cuối. Ví dụ để hiệu ứng một widget biến mất thì giá trị đầu của
nó có opacity( độ đục ) tuyệt đối và giá trị cuối có opacity bằng 0
2. Giá trị trung gian có thể là tuyến tính( đường thẳng) hoặc không tuyến tính (đường cong) và nó có
thể được cấu hình. Chúng ta hiểu rằng animation làm việc giống như được cấu hình. Với mỗi cấu
hình khác nhau sẽ cho ra một kiểu animation khác nhau. Ví dụ một widget hiệu ứng mờ dần từ trái
sang phải có thể cấu hình thành hiệu ứng nảy lên giống như quả bóng rồi dần dần biến mất
3. Thời gian khi chạy animation có tác động đến tốc độ (nhanh hay chậm) của hiệu ứng
4. Trong flutter, hệ thống animation không có bất kì animation cụ thể nào cả. Thay vào đó nó cung
cấp duy nhất giá trị yêu cầu cho tất cả các frame để render hình ảnh
Lớp Animation:
Flutter animation dựa trên các đối tượng animation. Lõi của các lớp animation nó hoạt động như sau :
Animation:
Tạo ra giá trị và được thêm vào giữa hai số (bắt đầu và kết thúc animation). Các kiểu animation thường
được sử dụng là:
Animation<double> : Thêm các giá trị giữa hai số thập phân
Animation<Color> : Thêm các màu vào giữa hai màu
Animation<Size> : Thêm kích thước vào giữa hai kích thước
AnimationController : Là một đối tượng animation đặc biệt dùng để điều khiển các hiệu ứng của chính
nó. Nó tạo ra các giá trị mới bất cứ khi nào ứng dụng sẵn sàng cho một frame mới.Ngoài ra nó còn hỗ trợ
các animation tuyến tính với giá trị từ 0.0 đến 1.0
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
Ở đây, controller kiểm soát hiệu ứng trong khoảng thời gian animation hoạt động. vsynv là một tính năng
đặc biệt được dùng để tối ưu hóa các nguồn sử dụng animation
CurvedAnimation
Nó cơ bản giống như AnimationController nhưng hỗ trợ animation phi tuyến tính( đường cong).
CurvedAnimation có thể sử dụng cùng với đối tượng animation như sau :
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
Tween<T>
Được kế thừa từ Animatable<T> và tạo các giá trị bất kì khác 0 và 1. Nó được sử dụng cùng với đối tượng
animation bởi phương thức animate
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this); Animation<int> customTween = IntTween(
begin: 0, end: 255).animate(controller);
Ngoài ra, Tween cũng có thể sử dụng cùng với CurvedAnimation như dưới đây :
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> customTween = IntTween(begin: 0, end: 255).animate(curve);
Nhìn trên, controller thực tế là một animation controller, curve cung cấp các dạng phi tuyến tính và
customTween để điều chỉnh giá trị trị từ 0 đến 255.
Đầu tiên, định nghĩa controller animation ở hàm initState trong StatefulWidget
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller);
controller.forward();
Thêm animation bằng listener, addListener để thay đổi trạng thái của widget
animation = Tween<double>(begin: 0, end: 300).animate(controller) ..addListener(() {
setState(() {
// The state that has changed here is the animation object’s value.
});
});
-Xây dựng widget gồm hai hàm là hàm AnimatedWidget và AnimatedBuilder . Cả hai widget đều chấp
nhận đối tượng animation và nhận giá trị hiện tại cần thiết cho animation
-Nhận các giá trị animation trong khi chạy các widget và sau đó sử dụng nó cho độ dài(height) và độ
rộng(width) hoặc bất kỳ các thuộc tính liên quan thay vì các giá trị ban đầu
child: Container(
height: animation.value,
width: animation.value,
child: <Widget>,
)
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Product Listing")),body: ListView(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0),
children: <Widget>[
FadeTransition(
child: ProductBox(
name: "iPhone",
description: "iPhone is the stylist phone ever",
price: 1000,
image: "iphone.jpg"
), opacity: animation
),
MyAnimatedWidget(child: ProductBox(
name: "Pixel",
description: "Pixel is the most featureful phone ever",
price: 800,
image: "pixel.jpg"
), animation: animation),
ProductBox(
name: "Laptop",
description: "Laptop is most productive development tool",
price: 2000,
image: "laptop.jpg"
),
ProductBox(
name: "Tablet",
description: "Tablet is the most useful device ever for meeting",
price: 1500,
image: "tablet.jpg"
),
ProductBox(
name: "Pendrive",
description: "Pendrive is useful storage medium",
price: 100,
image: "pendrive.jpg"
),
ProductBox(
name: "Floppy Drive",
description: "Floppy drive is useful rescue storage medium",
price: 20,
image: "floppydrive.jpg"
),
],
)
);
}
}
Ở đây chúng ta sử dụng FadeAnimation và MyAnimationWidget để tạo hiệu ứng cho 2 items đầu trong list
các product. FadeAnimation được xây dựng trong lớp animation mà chúng ta đã từng tạo hiệu ứng cho
widget con đang sử dụng opacity
Vậy là xong, dưới đây là toàn bộ code chúng ta đã hao
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 10), vsync: this);
animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
controller.forward();
}
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
controller.forward();
return MaterialApp(
title: 'Flutter Demo', theme: ThemeData(primarySwatch: Colors.blue,),
home: MyHomePage(title: 'Product layout demo home page', animation:
animation,)
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key key, this.title, this.animation}): super(key: key);
final String title;
final Animation<double> animation;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Product Listing")),
body: ListView(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0),
children: <Widget>[
FadeTransition(
child: ProductBox(
name: "iPhone",
description: "iPhone is the stylist phone ever",
price: 1000,
image: "iphone.jpg"
),
opacity: animation
),
MyAnimatedWidget(
child: ProductBox(
name: "Pixel",
description: "Pixel is the most featureful phone ever",
price: 800,
image: "pixel.jpg"
),
animation: animation
),
ProductBox(
name: "Laptop",
description: "Laptop is most productive development tool",
price: 2000,
image: "laptop.jpg"
),
ProductBox(
name: "Tablet",
description: "Tablet is the most useful device ever for meeting",
price: 1500,
image: "tablet.jpg"
),
ProductBox(
name: "Pendrive",
description: "Pendrive is useful storage medium",
price: 100,
image: "pendrive.jpg"
),
ProductBox(
name: "Floppy Drive",
description: "Floppy drive is useful rescue storage medium",
price: 20,
image: "floppydrive.jpg"
),
],
)
);
}
}
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.name, this.description, this.price, this.image}) :
super(key: key);
final String name;
final String description;
final int price;
final String image;
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(2),
height: 140,
child: Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Image.asset("assets/" + image),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(
this.name, style: TextStyle(
fontWeight: FontWeight.bold
)
),
Text(this.description), Text(
"Price: " + this.price.toString()
),
],
)
)
)
]
)
)
);
}
}
class MyAnimatedWidget extends StatelessWidget {
MyAnimatedWidget({this.child, this.animation});
final Widget child;
final Animation<double> animation;
Giao thức thông báo sử dụng mã thông báo tiêu chuẩn( lớp StandardMessageCodec), được hỗ trợ tuần tự
nhị phân của JSON - như các giá trị kiểu số, chuỗi, boolean,..serialization và de-serialization hoạt động rõ
ràng giữa Clients và Host.
Hôm nay chúng ta sẽ thử viết ứng dụng đơn giản để mở trình duyệt web sử dụng Android SDK
Đầu tiên chúng ta sẽ tạo ứng dụng với tên "flutter_browser_app" nhé
Sau đó thay thế đoạn code trong hàm main.dart thành :
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
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: Center(
child: RaisedButton(
child: Text('Open Browser'),
onPressed: null,
),
),
);
}
}
Ở đoạn code trên, ta đã tạo một nút có chức năng để mở trình duyệt và tạm thời ta set nó ở trạng thái
NULL
Bây giờ ta sẽ import đoạn code dưới đây :
import 'dart:async';
import 'package:flutter/services.dart';
Tiếp theo ta sẽ viết phương thức _openBrowser để gọi nền tảng cụ thể thông qua message channel.
Future<void> _openBrowser() async {
try {
final int result = await platform.invokeMethod(
'openBrowser', <String, String>{
'url': "https://flutter.dev"
}
);
}
on PlatformException catch (e) {
// Unable to open the browser
print(e);
}
}
Ở đây chúng ta sử dụng platform.invokeMethod để gọi openBrowser (giải thích ở bước tiếp theo ),
openBrowser có đối số, url để mở url cụ thể
Giờ chúng ta sẽ thay đổi đối số null trong RaiseButton thành _openBrowser
onPressed: _openBrowser,
Tiếp đến bạn hãy mở MainActivity,java (bên trong thư mục android) và import một số thư viện sau đây:
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugins.GeneratedPluginRegistrant;
Bây giờ chúng ta sẽ viết hàm openBrowser để mở trình duyệt nhé :
private void openBrowser(MethodCall call, Result result, String url) {
Activity activity = this;
if (activity == null) {
result.error("ACTIVITY_NOT_AVAILABLE",
"Browser cannot be opened without foreground
activity", null);
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
activity.startActivity(intent);
result.success((Object) true);
}
Trong hàm MainActivity ta đặt tên Channel;
private static final String CHANNEL = "flutterapp.tutorialspoint.com/browser";
Ở hàm Oncreate ta sẽ viết mã cụ thể cho Android để xử lý message
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Result result) {
String url = call.argument("url");
if (call.method.equals("openBrowser")) {
openBrowser(call, result, url);
} else {
result.notImplemented();
}
}
})
Nhìn trên, chúng ta đã tạo message channel sử dụng lớp MethodChannel và lớp MethodCallHandler để
xử lý thông báo. onMethodCall có trách nhiệm gọi đúng mã nền tảng riêng biệt bằng cách kiểm tra thông
báo. Hàm onMethodCall đọc url từ thông báo và gọi đến openBrowser khi mà hàm gọi openBrowser.
Ngược lại hàm sẽ trả về method notImplemented
OKI giờ chúng ta xem toàn bộ code nhé :
MainActivity.java
package com.tutorialspoint.flutterapp.flutter_browser_app;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugins.GeneratedPluginRegistrant;
main.dart
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// [GeneratedPluginRegistrant registerWithRegistry:self];
// Override point for customization after application launch.
return [super application:application
didFinishLaunchingWithOptions:launchOptions];
}
@end
Chúng ta sẽ thêm hàm openBrowser để mở trình duyệt web với url. Nó chấp nhận đối số duy nhất là url
- (void)openBrowser:(NSString *)urlString {
NSURL *url = [NSURL URLWithString:urlString];
UIApplication *application = [UIApplication sharedApplication];
[application openURL:url];
}
Trong hàm didFinishLaunchingWithOptions, tìm cotroller và đặt nó vào bên trong biến controller
FlutterViewController* controller =
(FlutterViewController*)self.window.rootViewController;
Trong hàm didFinishLaunchingWithOptions, đặt browser chanel là flutterapp.tutorialspoint.com/browse
FlutterMethodChannel* browserChannel = [
FlutterMethodChannel methodChannelWithName:
@"flutterapp.tutorialspoint.com/browser" binaryMessenger:controller];
Tạo biến weakSelf và đặt class hiện tại
__weak typeof(self) weakSelf = self;
Bây giờ ta sẽ implement setMethodCallHandler. Gọi hàm openBrowser bởi call.method. Lấy giá trị url
bằng call.arguments và bỏ qua nó khi gọi openBrowser
[browserChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result)
{
if ([@"openBrowser" isEqualToString:call.method]) {
NSString *url = call.arguments[@"url"];
[weakSelf openBrowser:url];
} else { result(FlutterMethodNotImplemented); }
}];
Dưới đây là toàn bộ code mẫu, mời bạn đọc cùng tham khảo
#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if ([@"openBrowser" isEqualToString:call.method]) {
NSString *url = call.arguments[@"url"];
[weakSelf openBrowser:url];
} else { result(FlutterMethodNotImplemented); }
}];
// custom code ends
[GeneratedPluginRegistrant registerWithRegistry:self];
1. Mở project setting
2. Tìm đến Capabilities bật Background Modes.
3. Thêm *Background fetch và Remote Notification**
4. Nào, bây giờ ta thử run ứng dụng và nó sẽ làm việc giống như phiên bản Android nhưng là trình
duyệt Safari thay vì chrome nhé
Vậy là chúng ta đã hoàn thành bài học hôm nay.
Kể từ khi Dart package là một collection có chức năng tương tự, nó có thể được phân loại dựa trên chức
năng:
Dart Package
Chúng ta có thể sử dụng Dart trên cả 2 môi trường là web và android. Ví dụ, english_words là một
package chứa khoảng 500 từ và có chức năng tiện ích cơ bản như danh từ ( list các danh từ trong
English), âm tiết (liệt kê ra các từ có âm tiết đặc biệt )
Flutter package
Phụ thuộc vào Flutter framework và có thể chỉ sử dụng trong môi trường mobile.
Flutter plugin
Phụ thuộc vào Flutter framework cũng như nền tảng cơ bản (Android SDK hay iOS SDK). Ví dụ Camera là
một plugin (có thể hiểu là một phần mềm hỗ trợ) để tương tác với thiết bị camera. Nó sử dụng SDK để có
quyền truy cập vào camera
Dart package được lưu trữ và publish trên các máy chủ, https://pub.dev. Ngoài ra, Flutter cung cấp các
tool, pub cơ bản để quản lý các Dart package trong ứng dụng. Các bước cần để sử dụng Package như sau
:
-Nhập tên package và phiên bản phù hợp trong file pubspec.yaml như dưới đây :
dependencies: english_words: ^3.1.5
-Bản mới nhất sẽ được cập nhật trên server
- Cài đặt package bằng lệnh :
flutter packages get
- Khi chúng ta đang dùng Android studio, thì Android studio sẽ phát hiện bất kỳ thay đổi trong file
pubspec.yaml và hiện thông báo để lập trình viên có thể biết
- Dart package có thể được cài đặt hoặc nâng cấp trong Android studio thông qua menu optionsoptions.
- Thêm các file cần thiết sử dụng lệnh dưới đây và bắt đầu làm việc :
import 'package:english_words/english_words.dart';
- Sử dụng bất kì phương thức có sẵn
nouns.take(50).forEach(print);
- Ở trên ta đã dùng hàm nouns để lấy ra 50 từ đầu tiên
my_browser.dart
import 'dart:async';
import 'package:flutter/services.dart';
class MyBrowser {
static const MethodChannel _channel = const MethodChannel('my_browser');
static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion'); return
version;
}
Future<void> openBrowser(String urlString) async {
try {
final int result = await _channel.invokeMethod(
'openBrowser', <String, String>{'url': urlString});
}
on PlatformException catch (e) {
// Unable to open the browser print(e);
}
}
}
MyBrowserPlugin.java
package com.tutorialspoint.flutterplugins.my_browser;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
/** MyBrowserPlugin */
public class MyBrowserPlugin implements MethodCallHandler {
private final Registrar mRegistrar;
private MyBrowserPlugin(Registrar registrar) {
this.mRegistrar = registrar;
}
/** Plugin registration. */
public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(
registrar.messenger(), "my_browser");
MyBrowserPlugin instance = new MyBrowserPlugin(registrar);
channel.setMethodCallHandler(instance);
}
@Override
public void onMethodCall(MethodCall call, Result result) {
String url = call.argument("url");
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
}
else if (call.method.equals("openBrowser")) {
openBrowser(call, result, url);
} else {
result.notImplemented();
}
}
private void openBrowser(MethodCall call, Result result, String url) {
Activity activity = mRegistrar.activity();
if (activity == null) {
result.error("ACTIVITY_NOT_AVAILABLE",
"Browser cannot be opened without foreground activity", null);
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
activity.startActivity(intent);
result.success((Object) true);
}
}
- Tạo project mới, lấy tên tuỳ bạn, ở đây mình sẽ dùng my_browser_plugin_test để đặt tên cho project
- Trong file pubspec.yaml ta thêm plugin my_browser trong dependency.
dependencies:
flutter:
sdk: flutter
my_browser:
path: ../my_browser
- Android studio sẽ thông báo rằng file pubspec.yam cần được cập nhật và hiển thị bạn chỉ cần click vào
get dependency
- Trong file main.dart và my_browser plugin ta cần thêm thư viện vào :
import 'package:my_browser/my_browser.dart';
- Gọi hàm openBrowser từ my_browser plugin như sau :
onPressed: () => MyBrowser().openBrowser("https://flutter.dev"),
Okey, dưới đây là toàn bộ code trong hàm main.dartdart
import 'package:flutter/material.dart';
import 'package:my_browser/my_browser.dart';
Gói http cung cấp các lớp cấp cao và http request từ web
- Lớp http cung cấp chức năng để làm việc với tất cả các kiểu dữ liệu HTTP được request
- Phương thức http có sử dụng url, và bổ sung thông tin thông qua Dart Map ( post dữ liệu, bổ sung tiêu
đề, ...). Nó yêu cầu lên máy chủ và thu thập phản hồi với async/await. Ví dụ đoạn code dưới đây đọc dữ
liệu từ url và in nó trên console
print(await http.read('https://flutter.dev/'));
Một vài phương thức chính :
- read : gửi yêu cầu lên sever thông qua phương thức GET và trả về Future<String>
- get : gửi yêu cầu lên sever thông qua phương thức GET và trả về Future<Response>. Response là lớp giữ
lại các thông tin phản hồi
- post : gửi yêu cầu lên sever thông qua phương thức POST bằng việc đưa giá trị lên sever và phản hồi
Future<Response>
- put : gửi yêu cầu lên sever thông qua phương thức PUT và trả về phản hồi như Future<Response>
- head : gửi yêu cầu lên sever thông qua phương thức HEAD và trả về phản hồi như Future<Response>
- delete : gửi yêu cầu lên sever thông qua phương thức DELETE và trả về phản hồi như Future<Response>
Http cũng cung cấp nhiều lớp standard HTTP client. client có nhiệm vụ hỗ trợ kết nối. Nó sẽ hữu ích khi có
rất nhiều request lên sever
var client = new http.Client();
try {
print(await client.get('https://flutter.dev/'));
}
finally {
client.close();
}
Truy cập vào Product service API
Ta sẽ tạo ứng dụng đơn giản để lấy dữ liệu Product từ web server và sau đó hiển thị danh sách Product
trong ListView
Tạo ứng dụng Flutter trong android với tên "product_rest_app"
Copy assets dưới đây vào pubspec.yaml,(các bạn có thể lấy ảnh ở bài 7 )
flutter:
assets:
- assets/appimages/floppy.jpg
- assets/appimages/iphone.jpg
- assets/appimages/laptop.jpg
- assets/appimages/pendrive.jpg
- assets/appimages/pixel.jpg
- assets/appimages/tablet.jpg
Thêm http package vào file pubspec.yaml như sau :
dependencies:
http: ^0.12.0+2
Thêm một vào package trong hàm main.dart
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
Tạo file JSON, product.json
[
{
"name": "iPhone",
"description": "iPhone is the stylist phone ever",
"price": 1000,
"image": "iphone.png"
},
{
"name": "Pixel",
"description": "Pixel is the most feature phone ever",
"price": 800,
"image": "pixel.png"
},
{
"name": "Laptop",
"description": "Laptop is most productive development tool",
"price": 2000,
"image": "laptop.png"
},
{
"name": "Tablet",
"description": "Tablet is the most useful device ever for meeting",
"price": 1500,
"image": "tablet.png"
},
{
"name": "Pendrive",
"description": "Pendrive is useful storage medium",
"price": 100,
"image": "pendrive.png"
},
{
"name": "Floppy Drive",
"description": "Floppy drive is useful rescue storage medium",
"price": 20,
"image": "floppy.png"
}
]
Tạo thư mục mới tên là JSONWebServer, đặt products.json vào trong
Chạy bất kỳ web server với JSONWebServer và lấy đường dẫn. Ví dụ
http://192.168.184.1:8000/products.json. Một vài web server như apache, nginx, ...
Cách đơn giản nhất là cài đặt nodejs dựa trên ứng dụng http-server. Các bước cài đặt như sau :
- Cài đặt ứng dụng NodeJs https://nodejs.org/en
- Đi đến thư mục JSONWebServer
cd /path/to/JSONWebServer
- Cài đặt http-server package bằng cách sử dụng npm
npm install -g http-server
- Sau đó thử chạy server
http-server . -p 8000
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return GestureDetector(
child: ProductBox(item: items[index]),
onTap: () {
Navigator.push(
context, MaterialPageRoute(
builder: (context) =gt; ProductPage(item: items[index]),
),
);
},
);
},
);
}
}
Lưu ý rằng, chúng ta sử dụng cùng nội dung trong ứng dụng Navigation để đưa ra list Product
Cuối cùng ta sửa đổi MyHomePage widget's để lấy thông tin Product sử dụng tính năng Future thay vì
phương thức gọi thông thường
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Product Navigation")),
body: Center(
child: FutureBuilder<List<Product>>(
future: products, builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData ? ProductBoxList(items: snapshot.data)
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return GestureDetector(
child: ProductBox(item: items[index]),
onTap: () {
Navigator.push(
context, MaterialPageRoute(
builder: (context) => ProductPage(item: items[index]),
),
);
},
);
},
);
}
}
class ProductPage extends StatelessWidget {
ProductPage({Key key, this.item}) : super(key: key);
final Product item;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(this.item.name),),
body: Center(
child: Container(
padding: EdgeInsets.all(0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.asset("assets/appimages/" + this.item.image),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(this.item.name, style:
TextStyle(fontWeight: FontWeight.bold)),
Text(this.item.description),
Text("Price: " + this.item.price.toString()),
RatingBox(),
],
)
)
)
]
),
),
),
);
}
}
class RatingBox extends StatefulWidget {
@override
_RatingBoxState createState() =>_RatingBoxState();
}
class _RatingBoxState extends State<RatingBox> {
int _rating = 0;
void _setRatingAsOne() {
setState(() {
_rating = 1;
});
}
void _setRatingAsTwo() {
setState(() {
_rating = 2;
});
}
void _setRatingAsThree() {
setState(() {
_rating = 3;
});
}
Widget build(BuildContext context) {
double _size = 20;
print(_rating);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 1
? Icon(Icons.star, ize: _size,)
: Icon(Icons.star_border, size: _size,)
),
color: Colors.red[500], onPressed: _setRatingAsOne, iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 2
? Icon(Icons.star, size: _size,)
: Icon(Icons.star_border, size: _size, )
),
color: Colors.red[500],
onPressed: _setRatingAsTwo,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 3 ?
Icon(Icons.star, size: _size,)
: Icon(Icons.star_border, size: _size,)
),
color: Colors.red[500],
onPressed: _setRatingAsThree,
iconSize: _size,
),
),
],
);
}
}
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.item}) : super(key: key);
final Product item;
SQLite
SQLite là một SQL tiêu chuẩn dựa trên công cụ cơ sở dữ liệu nhúng. Nó là công cụ nhỏ và đang được thử
nghiệm theo thời gian. Gói sqflite cung cấp nhiều chức năng để làm việc hiệu quả với SQLite database. Nó
cung cấp các phương thức tiêu chuẩn để vận hành SQLite database. Chức năng chính của sqflite như sau
:
1. Tạo/mở SQLite database
2. Thực thi SQL statement (thực thi phương thức) đối với SQLite database
3. Phương thức truy vấn nâng cao (phương thức truy vấn) để giảm code cần thiết để truy vấn và lấy
thông tin từ SQLite database.
Bây giờ ta thử tạo ứng dụng cửa hàng sản phẩm điện thoại và lấy dữ liệu từ SQLite database sử dụng gói
sqflite và hiểu các khái niệm về SQLite database và gói sqflite.
Tạo một ứng dụng mới có tên là : product_sqlite_app
Copy thư mục assets bài 7 sang và thêm vào mục *pubspec.yaml`
flutter:
assets:
- assets/appimages/floppy.png
- assets/appimages/iphone.png
- assets/appimages/laptop.png
- assets/appimages/pendrive.png
- assets/appimages/pixel.png
- assets/appimages/tablet.png
Cấu hình sqflite trong file pubspec.yaml như dưới đây
dependencies: sqflite: any
Sử dụng phiên bản mới nhất của sqflite điền vào chỗ any
Android studio sẽ thông báo rằng pubspec.yaml cần được cập nhật rồi ta nhấn và Get dependencies.
Android studio sẽ lấy package từ internet và cấu hình thuộc tính cho ứng dụng
Trong database(cơ sở dữ liệu), chúng ta cần primary key, id như trường bổ sung kèm với các thuộc tính
của Product như : tên, giá, ... Vì thế, thêm thuộc tính id vào lớp Product. Ngoài ra, thêm 1 phương thức mới
là toMap để chuyển đổi đối tượng product vào Map.
class Product {
final int id;
final String name;
final String description;
final int price;
final String image;
static final columns = ["id", "name", "description", "price", "image"];
Product(this.id, this.name, this.description, this.price, this.image);
factory Product.fromMap(Map<String, dynamic> data) {
return Product(
data['id'],
data['name'],
data['description'],
data['price'],
data['image'],
);
}
Map<String, dynamic> toMap() => {
"id": id,
"name": name,
"description": description,
"price": price,
"image": image
};
}
Tạo một file mới Database.dart để viết SQLite
Thêm một vài thư viện cần thiết trong Database.dart
import 'dart:async';
import 'dart:io';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'Product.dart';
Ta cần chú ý một vài điểm sau :
1. async được sử dụng để viết phương thức asynchronous(không đồng bộ)
2. io được sử dụng để truy cập vào tệp và thư mục
3. path được sử dụng để truy cập vào tiện ích cốt lõi của dart liên quan đến đường dẫn tệp
4. path_provider được sử dụng để lấy đường dẫn tạm thời
5. sqflite được sử dụng để vận hành database SQLite
Tạo một lớp mới có tên SQLiteDbProvider
class SQLiteDbProvider {
SQLiteDbProvider._();
static final SQLiteDbProvider db = SQLiteDbProvider._();
static Database _database;
}
SQLiteDBProvoider là phương thức có thể truy cập thông qua biến đổi vùng nhớ db
SQLiteDBProvoider.db.<emthod>
Tạo hàm để lấy database (sử dụng Future) của kiểu Future<Database>. Tạo bảng product và tải dữ liệu
ban đầu trong quá trình tạo 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, "ProductDB.db");
return await openDatabase(
path,
version: 1,
onOpen: (db) {},
onCreate: (Database db, int version) async {
await db.execute(
"CREATE TABLE Product ("
"id INTEGER PRIMARY KEY,"
"name TEXT,"
"description TEXT,"
"price INTEGER,"
"image TEXT" ")"
);
await db.execute(
"INSERT INTO Product ('id', 'name', 'description', 'price', 'image')
values (?, ?, ?, ?, ?)",
[1, "iPhone", "iPhone is the stylist phone ever", 1000, "iphone.png"]
);
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", "Pendrive is useful storage medium", 100, "pendrive.png"]
);
await db.execute(
"INSERT INTO Product
('id', 'name', 'description', 'price', 'image')
values (?, ?, ?, ?, ?)",
[6, "Floppy Drive", "Floppy drive is useful rescue storage medium", 20,
"floppy.png"]
);
}
);
}
Chúng ta đã sử dụng một số phương thức như sau :
getApplicationDocumentsDirectory: trả về đường dẫn thư mục ứng dụng
join : sử dụng để tạo đường dẫn cụ thể. Ta đã sử dụng nó để tạo đường dẫn database
openDatabase : sử dụng để mở SQLite database
onOpen : Được dùng để viết code khi mở database
onCreate : Sử dụng để viết code khi database được tạo lần đầu
db.execute : Sử dụng để thực thi truy vấn SQL
Viết hàm để lấy tất cả product từ database
Future<List<Product>> getAllProducts() async {
final db = await database;
List<Map>
results = await db.query("Product", columns: Product.columns, orderBy: "id ASC");
var id = maxIdResult.first["last_inserted_id"];
var result = await db.rawInsert(
"INSERT Into Product (id, name, description, price, image)"
" VALUES (?, ?, ?, ?, ?)",
[id, product.name, product.description, product.price, product.image]
);
return result;
}
update(Product product) async {
final db = await database;
var result = await db.update("Product", product.toMap(),
where: "id = ?", whereArgs: [product.id]); return result;
}
delete(int id) async {
final db = await database;
db.delete("Product", where: "id = ?", whereArgs: [id]);
}
Database.dart như sau :
import 'dart:async';
import 'dart:io';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'Product.dart';
class SQLiteDbProvider {
SQLiteDbProvider._();
static final SQLiteDbProvider db = SQLiteDbProvider._();
static Database _database;
Cloud Firestore
Firebase là nền tảng phát triển ứng dụng Baas(Backend-as-a-Service). Nhiều lập trình viên trên thế giới đã
sử dụng mBaaS bởi vì nhiều lợi ích nó mang đến. Nó cung cấp nhiều tính năng để tăng tốc việc phát triển
ứng dụng như xác thực, lưu trữ đám mây,... Một trong những tính năng chính của Firebase là Cloud
Firestore, là cloud dựa trên NoSQL database với thời gian thực
Flutter cung cấp gói cụ thể, cloud_firestore để làm việc với Cloud Firebase. Chúng ta hãy tạo cửa hàng
product trực tuyến trên Cloud Firestore và tạo ứng dụng để truy cập vào.
1. Tạo ứng dụng flutter mới tên là product_firebase_app
2. Cooy tệp Product.dart từ thư mục product_rest_app sang
class Product {
final String name;
final String description;
final int price;
final String image;
List<Product>
items = List<Product>();
@override
bool isSupported(Locale locale) => ['en', 'es'].contains(locale.languageCode);
@override
Future<CustomLocalizations> load(Locale locale) {
return SynchronousFuture<CustomLocalizations>(CustomLocalizations(locale));
}
@override bool shouldReload(CustomLocalizationsDelegate old) => false;
}
Ở trên, CustomLocalizations được tạo để hỗ trợ miền cho tiêu đề và thông báo trong ứng dụng và
CustomLocalizationsDelegate được sử dụng để tải CustomLocalizations
Thêm quyền cho MaterialApp, WidgetsApp và CustomLocalization sử dụng thuộc tính MaterialApp,
localizationsDelegates và supportedLocales như sau :
localizationsDelegates: [
const CustomLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''),
const Locale('es', ''),
],
Sử dụng phương thức CustomLocalizations, of để lấy giá trị của localized và sử dụng nó thích hợp như
sau :
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(CustomLocalizations .of(context) .title), ),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text( CustomLocalizations .of(context) .message, ),
],
),
),
);
}
}
Nhìn trên, ta đã sửa đổi lớp MyHomePage từ StatefulWidget sang StatelessWidget và sử dụng
CustomLocalizations để lấy tiêu đề, thông điệp.
Biên dịch và chạy ứng dụng. Ứng dụng sẽ hiển thị nội dung là tiếng Anh.
Đóng ứng dụng. Vào Settings → System → Languages and Input → Languages*
Nhấn thêm ngôn ngữ và lựa chọn Spanish. Điện thoại sẽ cài đặt ngôn ngữ Spanish.
Lựa chọn Spanish và di chuyển nó lên trên English. Nó sẽ mặc định chọn ngôn ngữ Spanish là ngôn ngữ
đầu tiên và tất cả sẽ được chuyển sang ngôn ngữ Spanish.
Chúng ta có thể thay đổi lại ngôn ngữ tiến Anh bằng cách tương tự là di chuyển English lên trên đầu trong
cài đặt.
Và kết quả hiển thị như sau:
Sử dụng gói intl
Flutter cung cấp intl package để đơn giản việc phát triển localized trong ứng dụng di động.intl package
cung cấp phương thức đặc biệt và công cụ để tạo bán tự động ngôn ngữ thông điệp cụ thể.
Chúng ta sẽ tạo ứng dụng localized mới bằng việc sử dụng intl package và hiểu về ý tưởng của package
này.
- Tạo ứng dụng flutter mới với tên "flutter_intl_app"
- Mở file pubspec.yaml và thêm package như sau :
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.15.7
intl_translation: ^0.17.3
Get dependencies khi Android studio hiển thị thông báo cập nhật thay đổi
- Copy hàm main.dart trong ví dụ trước flutter_internationalization_app
- Import intl package như sau
import 'package:intl/intl.dart';
Cập nhật lớp CustomLocalization :
class CustomLocalizations {
static Future<CustomLocalizations> load(Locale locale) {
final String name = locale.countryCode.isEmpty ? locale.languageCode :
locale.toString();
final String localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
return CustomLocalizations();
});
}
static CustomLocalizations of(BuildContext context) {
return Localizations.of<CustomLocalizations>(context, CustomLocalizations);
}
String get title {
return Intl.message(
'Demo',
name: 'title',
desc: 'Title for the Demo application',
);
}
String get message{
return Intl.message(
'Hello World',
name: 'message',
desc: 'Message for the Demo application',
);
}
}
class CustomLocalizationsDelegate extends
LocalizationsDelegate<CustomLocalizations> {
const CustomLocalizationsDelegate();
@override
bool isSupported(Locale locale) => ['en', 'es'].contains(locale.languageCode);
@override
Future<CustomLocalizations> load(Locale locale) {
return CustomLocalizations.load(locale);
}
@override
bool shouldReload(CustomLocalizationsDelegate old) => false;
}
Ở trên, ta đã sử dụng 3 phương thức từ intl package thay vì phương thức tự tạo.
1. Intl.canonicalizedLocale - được sử dụng để lấy chính xác tên ngôn ngữ
2. Intl.defaultLocale - Sử dụng để set ngôn ngữ hiện tại
3. Intl.message - Sử dụng để định nghĩa thông điệp mới
import l10n/messages_all.dart file
import 'l10n/messages_all.dart';
Tiếp theo, ta tạo thư mục lib/l10n
Mở command prompt và đi đến ứng dụng thư mục gốc (pubspec.yaml) và chạy đoạn command sau :
flutter packages pub run intl_translation:extract_to_arb --output-
dir=lib/l10n lib/main.dart
Ở đây, lệnh được tạo intl_message.arb file, Một bản mẫu để tạo thông điệp trong ngôn ngữ khác. Nội dung
của file như sau :
{
"@@last_modified": "2019-04-19T02:04:09.627551",
"title": "Demo",
"@title": {
"description": "Title for the Demo application",
"type": "text",
"placeholders": {}
},
"message": "Hello World",
"@message": {
"description": "Message for the Demo
application",
"type": "text",
"placeholders": {}
}
}
Copy intl_message.arb và tạo file mới, intl_en.arb và thay đổi nội dung sang ngôn ngữ Spanish :
{
"@@last_modified": "2019-04-19T02:04:09.627551",
"title": "Manifestación",
"@title": {
"description": "Title for the Demo application",
"type": "text",
"placeholders": {}
},
"message": "Hola Mundo",
"@message": {
"description": "Message for the Demo application",
"type": "text",
"placeholders": {}
}
}
Oke, bây giờ ta thử run lệnh để tạo tệp cuối cùng, messages_all.dart.
flutter packages pub run intl_translation:generate_from_arb
--output-dir=lib\l10n --no-use-deferred-loading
lib\main.dart lib\l10n\intl_en.arb lib\l10n\intl_es.arb
Biên dịch và run app. Nó sẽ hoạt động như ứng dụng flutter_localization_app ở trên.
Unit Testing
Là phương pháp testing đơn giản nhất. Nó dựa trên việc đảm bảo độ chính xác của một đoạn code.
Nhưng nó hoạt động không thực sự tốt trên môi trường thật nên nó ít được sử dụng trong việc tìm lỗi
Widget Testing
Được dựa trên việc đảm bảo độ chính xác trong việc tạo, render hay tương tác của widget với widget khác
như mong đợi. Nó hoạt động từng bước và cung cấp gần như thời gian thực trong việc tìm lỗi
Integration Testing
Integration testing bao gồm cả hai unit testing và widget testing cùng với các thành phần bên ngoài ứng
dụng như database, web service,.. Nó mô phỏng hoặc giả lập môi trường thực để tìm ra gần như tất cả
các lỗi. Vì thế nó là quá trình phức tạp nhất của ưng dụng
Flutter cung cấp, hỗ trợ tất cả các loại testing. Nó cung cấp gói mở rộng và hỗ trợ riêng cho Widget
testing. Trong bài này, chúng ta sẽ tập trung bàn luận chi tiết về widget testing
Widget Testing
Flutter testing framework cung cấp phương thức testWidgets để test widgets. Nó chấp nhận 2 tham số
- Test decription
- Test code
testWidgets('test description: find a widget', '<test code>')
expect(find.text('2'), findsOneWidget);
Nhấn Run menu
Nhấn test trong widget_test.dart. Nó sẽ chạy và hiển thị kết quả trong cửa sổ kết quả.
Android
- Thay đổi tên ứng dụng bằng cách truy cập vào lệnh android:label trong file manifest.
AndroidManifest.xml được đặt trong <app dir>.android/app/src/main. Trong file này chứa toàn bộ chi tiết
về ứng dụng Android. Chúng ta có thể đặt tên ứng dụng thông qua android:label
- Thay đổi icon của ứng dụng bằng android:icon trong file manifest
- Cách xuất ứng dụng sang file APK bằng lệnh sau :
cd /path/to/my/application
flutter build apk
Sau khi chạy dòng lệnh thì màn hình sẽ hiển thị như sau :
Initializing gradle... 8.6s
Resolving dependencies... 19.9s
Calling mockable JAR artifact transform to create file:
/Users/.gradle/caches/transforms-1/files-1.1/android.jar/
c30932f130afbf3fd90c131ef9069a0b/android.jar with input
/Users/Library/Android/sdk/platforms/android-28/android.jar
Running Gradle task 'assembleRelease'...
Running Gradle task 'assembleRelease'...
Done 85.7s
Built build/app/outputs/apk/release/app-release.apk (4.8MB).
- Cách cài đặt file APK trực tiếp trên thiết bị di động:
flutter install
`- Đẩy ứng dụng lên Google PlayStore bằng cách tạo appbundle và đẩy nó lên bằng lệnh sau :
flutter build appbundle
IOS
- Đầu tiên ta cần đăng ký tài khoản App Store Connect..Lưu ý lưu =Bundle ID đã đăng kí để sau này khi
update ứng dụng cần tới
- Cập nhật tên Display trong phần cài đặt project của XCode để đặt tên ứng dụng
- Cập nhật Bundle Identifier trong cài đặt của project Xcode để đặt bundle id mà ta sử dụng ở bước 1
- Thêm icon mới
- Tạo file IPA sử dụng lệnh sau
Widget Sets
Google cập nhật widget Material và Cupertino để cung cấp chất lượng độ phân giải tốt trong thành phần
thiết kế. Bản mới nhất là flutter 1.2 được thiết kế để hỗ trợ lắng nghe sự kiện từ bàn phím và chuột
Phát triển ứng dụng Flutter với Visual Studio Code
Visual Studio Code hỗ trợ trong việc phát triển ứng dụng flutter và cung cấp các phím tắt mở rộng để phát
triển một cách nhanh chóng và đạt hiệu quả cao. Một vài tính năng chính được cung cấp bởi Visual Studio
Code như sau :
1. Hỗ trợ code : Khi chúng ta muốn kiểm tra một tính năng, ta chỉ cần nhấn Ctrl+Space thì sẽ hiển thị
một list các tính năng phù hợp
2. Ctrl+ : công cụ sửa lỗi nhanh
3. Phím tắt khi coding
4. Cung cấp chi tiết chức năng và cách dùng trong comments
5. Phím tắt Debugging
6. Hot restarts
Dart DevTools
Ta có thể hoàn toàn sử dụng Android Studio hay Visual Studio Code, hoặc bất kỳ IDE khác để viết code và
cài đặt plugins. Đội ngũ phát triển của Google đã và đang làm việc với các công cụ phát triển khác gọi
Dart DevTools. Đó là bộ lập trình dựa trên web. Nó hỗ trợ cả hai nền tảng là Android và IOS.
Chạy Server
Bạn có thể chạy máy chủ DevTools như sau :
flutter packages pub global run devtools
Máy chủ sẽ phản hồi
Serving DevTools at http://127.0.0.1:9100
Flutter SDK
Để cập nhật Flutter SDK, ta sử dụng dòng lệnh sau :
flutter upgrade
Bạn có thể thấy kết quả như sau :
Để cập nhật các package trong flutter, ta dùng lệnh
Flutter Inspector
Nó được sử dụng để khám phá widget flutter ở dạng cây. Để làm được điều này, chạy dòng lệnh sau ở
console
flutter run --track-widget-creation
Bạn có thể thấy kết quả như sau :
Launching lib/main.dart on iPhone X in debug mode...
─Assembling Flutter resources... 3.6s
Compiling, linking and signing... 6.8s
Xcode build done. 14.2s
2,904ms (!)
To hot reload changes while running, press "r". To hot restart (and rebuild state),
press "R".
An Observatory debugger and profiler on iPhone X is available at:
http://127.0.0.1:50399/
For a more detailed help message, press "h". To detach, press "d"; to quit, press "q".
Tới url ở trên http://127.0.0.1:50399/, ta sẽ được kết quả như sau :
Bài 23: Viết ứng dụng hoàn chỉnh
Trong bài này, chúng ta sẽ tạo một ứng dụng hoàn chỉnh là expense_calculator. Mục đích của ứng dụng là
để lưu trữ thông tin chi phí. Để hoàn thành ứng dụng ta cần các tính năng sau đây :
1. Danh sách chi phí
2. Tạo mẫu để nhập chi phí mới
3. Tính năng chỉnh sửa/xóa
4. Tính tổng chi phí trong bất kì trường hợp nào
Ta sẽ làm việc với ứng dụng và sử dụng một vào tính năng tiên tiến của flutter framework
1. Sử dụng listview để hiển thị ra danh sách chi phí
2. Lập trình mẫu
3. Sử dụng SQLite để lưu trữ chi phí
4. Sử dụng quản lý vùng nhớ scoped_model để đơn giản hoá việc lập trình
Nào, chúng ta hãy bắt đầu với ứng dụng expense_calculator
- Tạo một ứng dụng flutter với tên expense_calculator trên android studio hoặc visual studio code tuỳ ý
- Mở file pubspec.yaml và thêm một vài package sau :
dependencies:
flutter:
sdk: flutter
sqflite: ^1.1.0
path_provider: ^0.5.0+1
scoped_model: ^1.0.1
intl: any
-Lưu ý một vài điểm sau :
● sqflite được dùng cho SQLite db
● path_provider được sử dụng để lấy đường dẫn hệ thống riêng của ứng dụng
● scoped_model dùng để quản lý vùng nhớ
● intl dùng để định dạng ngày
-Sau đó ta sẽ nhấn Get dependencies để cập nhật lại các package trong ứng dụng
-Xoá main.dart
-Tạo file mới là Expense.dart để tạo lớp Expense. Lớp Expense bao gồm các thuộc tính và phương thức
như sau
● property: id - mỗi chi phí được đưa vào SQLite db đều có một địa chỉ riêng và dùng id để phân
biệt
● property: amount - số lượng chi tiêu
● property: date - thời gian chi tiêu
● property: category - Dùng để phân biệt các đối tượng đã chi tiêu như : Food, Travel,..
● formattedDate - định dạng lại ngày
● fromMap - Dùng để map các trường trong db với các thuộc tính đối tượng expense và tạo ra đối
tượng expense mới
factory Expense.fromMap(Map<String, dynamic> data) {
return Expense(
data['id'],
data['amount'],
DateTime.parse(data['date']),
data['category']
);
}
● toMap - Sử dụng để chuyển đổi tôi tượng expense sang dart map mà xa hơn là sử dụng để lập
trình db
Map<String, dynamic> toMap() => {
"id" : id,
"amount" : amount,
"date" : date.toString(),
"category" : category,
};
● columns - đại diện cho một trường của db
- Truy cập và lưu đoạn code này vào 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,
};
}
- Thêm file mới có tên là Database.dart để tạo lớp SQLiteDbProvider. Mục đích của SQLiteDbProvider là :
● Lấy toàn bộ giá trị expenses trong db bằng phương thức getAllExpenses. Phương thức này cho
phép sử dụng list tất cả thông tin user expenses
Future<List<Expense>> getAllExpenses() async {
final db = await database;
return result.isNotEmpty ?
Expense.fromMap(result.first) : Null;
}
● Lấy toàn bộ expenses của user bằng phương thức getTotalExpense. Dùng để hiển thị toàn bộ user
ở thời điểm đó
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;
}
● Thêm một thông tin expense mới bằng phương thức insert. Có chức năng thêm một user mới
trong
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);
}
● Cập nhật thông tin của user expense bằng phương thức update. Ta có thể chỉnh sửa hoặc cập
nhật giá trị có sẵn của expense
update(Expense product) async {
final db = await database;
class SQLiteDbProvider {
SQLiteDbProvider._();
static final SQLiteDbProvider db = SQLiteDbProvider._();
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'),
),
],
),
),
),
);
}
}
- Ở đây :
● TextFormField được sử dụng để tạo một form
● thuộc tính validator của TextFormField được sử dụng để xác thực thành phần biểu mẫu cùng với
các mẫu RegEx.
● Hàm _submit được sử dụng cùng với expense để thêm hoặc cập nhật expenses trong db
- Toàn bộ code trong main.dart như sau :
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'),
),
],
),
),
),
);
}
}
● Bây giờ, ta thử chạy ứng dụng
● Thêm expense mới sử dụng nút "+"
● Chỉnh sửa expense bằng cách nhấn vào expense đó
● Xoá expense bằng cách lướt expense qua trái
Đây là kết quả