You are on page 1of 156

Giới thiệu Flutter1

Bài 1: Giới thiệu Flutter 3


Flutter là gì? 3
Giới thiệu Flutter 4
Tính năng của Flutter 4
Điểm mạnh của Flutter 4
Đối tượng tham gia khóa học 5

Bài 2: Cài đặt Flutter 5


Cài đặt Flutter trên Window 5
Cài đặt Flutter in MacOS 6

Bài 3: Tạo ứng dụng Flutter đầu tiên 6

Bài 4: Kiến trúc ứng dụng Flutter 14


Widgets 15
Gestures 16
Khái niệm State 16
Layers 16
Tổng kết 17

Bài 5: Giới thiệu ngôn ngữ Dart 17


Biến và Kiểu dữ liệu 18
Điều khiển và vòng lặp 18
Hàm (Functions) 19
Lập trình hướng đối tượng 19

Bài 6: Widget trong Flutter 20


Tổng quan về các loại Widget trong Flutter 21
Platform specific widgets 21
Layout widgets 23
State maintenance widgets 23
Platform independent / basic widgets 23

Bài 7: Layout trong Flutter 26


Single Child Widgets 26
Multiple Child Widgets 30
Ví dụ 30

Bài 8: Gesture trong Flutter 37

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

Bài 10: Statefulwidget trong Flutter 42

Bài 11: ScopedModel trong Flutter 49


Model 50
ScopedModel 50
Single model : 50
Multiple model 50
ScopedModelDescendant 51

Bài 12: Navigator và Routing 59


MaterialPageRoute 60
Navigation.push 60
Navigation.pop 60

Bài 13: Animation 74


Giới thiệu: 74
Lớp Animation: 74
Animation: 74
CurvedAnimation 75
Tween<T> 75
Bây giờ ta bắt đầu làm việc với Animation: 75
Làm việc với ứng dụng : 75

Bài 14: Code với native Android 85


MainActivity.java 87
main.dart 88

Bài 15: Code với native IOS 91

Bài 16: Giới thiệu về package 93


Các kiểu Package : 94
Dart Package 94
Flutter package 94
Flutter plugin 94
Sử dụng Dart Package : 94
Làm việc với Flutter plugin package 94
my_browser.dart 98
MyBrowserPlugin.java 99

Bài 17: REST API 103


Nội dung cơ bản : 104
Truy cập vào Product service API 104

Bài 18: Khái niệm về Database 112


SQLite 112
Cloud Firestore 119

Bài 19: Chuyển đổi ngôn ngữ 122


Sử dụng gói intl 127

Bài 20: Kiểm thử 129


Một số dạng Testing 129
Unit Testing 129
Widget Testing 129
Integration Testing 129
Widget Testing 129
Các bước thực hiện 130
Ví dụ cụ thể 131

Bài 21: Xuất ứng dụng trong Flutter 131


Android 131
IOS 132

Bài 22: Công cụ phát triển 132


Widget Sets 132
Phát triển ứng dụng Flutter với Visual Studio Code 133
Dart DevTools 133
Cài đặt DevTools 133
Chạy Server 133
Bắt đầu với ứng dụng của bạn 134
Bắt đầu với DevTools trong Browser 134
Flutter SDK 134
Flutter Inspector 135

Bài 23: Viết ứng dụng hoàn chỉnh 136

Bài 1: Giới thiệu Flutter


Flutter là gì?
Flutter là một framework mã nguồn mở cho phép tạo ứng dụng di động với hiệu năng cao, chất lượng tốt
hỗ trợ đa nền tảng, phù hợp với phát triển ứng dụng Android và iOS.
Sử dụng ngôn ngữ Dart của chính Google, Flutter rất dễ học, mạnh mẽ, hiệu năng cao và phát triển ứng
dụng di động một cách nhanh chóng.
Trong khóa học này, mình sẽ giúp các bạn làm quen với Flutter framework, hướng dẫn cài đặt Flutter SDK,
thiết lập Android Studio để xây dựng một ứng dụng Flutter căn bản, nắm vững kỹ thuật của Flutter
framework và có khả năng phát triển các loại ứng dụng khác nhau sử dụng Flutter framework.
Giới thiệu Flutter
Nhìn chung phát triển ứng dụng di động là một công việc phức tạp và nhiều khó khăn. Có rất nhiều
framework hỗ trợ bạn phát triển một ứng dụng di động. Android cung cấp một framework cơ bản dựa trên
ngôn ngữ lập trình Java còn iOS thì cung cấp framework dựa trên Objective-C / Swift.
Tuy nhiên hầu hết các ứng dụng hiện nay, đều hỗ trợ cả 2 nền tảng Android và iOS, do đó cùng lúc phát
triển 2 dự án khác nhau với 2 framework khác nhau là một công việc phức tạp và lãng phí thời gian công
sức. Do đó người ta đã phát triển các framework lập trình đa nền tảng để giải quyết vấn đề này. Một
framework rất phổ biến hiện nay là React Native được phát triển với Facebook đang được sử dụng rất
rộng rãi. Tuy nhiên React Native vẫn thông qua các api của các framework gốc như Android hay iOS do đó
bị hạn chế và tốc độ kém.
Như một sự phát triển của tương lai, Flutter được phát triển bới chính Google, đơn vị sở hữu Android như
một đối trọng trực tiếp với React Native. Thay vì gọi các api của framework gốc, Flutter tạo ra giao diện
trực tiếp từ api của hệ điều hành. Nhờ đó ứng dụng sẽ chạy nhanh hơn, mượt mà hơn và đẹp hơn.
Flutter cung cấp rất nhiều widgets (UI) là các thành phần đồ hoạ được thiết kế riêng. Những đối tượng đồ
hoạ này được tối ưu phù hợp với môi trường mobile và dễ dàng trong việc thiết kế như HTML.
Cụ thể, ứng dụng Flutter sẽ sử dụng các widget riêng. Flutter widgets cung cấp các animations (hiệu ứng)
và gestures (thao tác cử chỉ) riêng. Ứng dụng được phát triển dựa trên logic của reactive programming.
Mỗi Widget sẽ có rất nhiều trạng thái. Bằng cách thay đổi trạng thái của widget, Flutter sẽ tự động
(reactive programming) so sánh trạng thái của widget (cũ và mới) để tạo ra những thay đổi cần thiết thay
vì khởi tạo lại cả đối tượng.
Mình sẽ nói kỹ hơn về kĩ thuật này trong các bài tiếp theo.

Tính năng của Flutter


Flutter framework có những đặc điểm sau
1. Hiện đã và là một react framework
2. Sử dụng ngôn ngữ lập trình Dart đơn giản và dễ học
3. Phát triển ứng dụng nhanh
4. Giao diện người dùng rất đẹp và linh hoạt
5. Hỗ trợ rất nhiều widget khác nhau
6. Thể hiện cùng một UI trên nhiều nền tảng
7. Ứng dụng có hiệu năng cao

Điểm mạnh của Flutter


Flutter đi kèm với nhiều widget đẹp và có độ tùy biến cao giúp phát triển ứng dụng hiệu năng cao vượt trội
đáp ứng mọi nhu cầu và tuỳ biến. Bên cạnh đó Flutter còn có những điểm mạnh sau:
1. Dart có một kho lớn các gói phần mềm cho phép bạn mở rộng khả năng cho ứng dụng của mình
2. Các lập trình viên chỉ cần viết một chương trình duy nhất cho tất cả các ứng dụng (Android và
iOS). Flutter có thể mở rộng ra các nền tảng khác trong thời gian tới.
3. Flutter dễ dàng kiểm thử hơn do tiết kiệm thời gian kiểm thử trên từng nền tảng.
4. Nhờ sự đơn giản của mình, Flutter là lựa chọn hàng đầu cho các ứng dụng mới. Nó còn dễ dàng
tùy biến và mở rộng lên càng mạnh mẽ hơn
5. Với Flutter, lập trình viên có toàn quyền để sắp xếp bố trí điều khiển các widget
6. Flutter có bộ công cụ phát triển (developer tools) rất hoàn thiện và đầy đủ, đặc biệt với tính năng
hot reload đẩy nhanh tốc độ build ứng dụng đáng kinh ngạc

Đối tượng tham gia khóa học


Khóa học dành cho các bạn mới bắt đầu làm quen với Flutter và lập trình di động. Tuy nhiên nếu bạn đã
biết lập trình Android hoặc IOS native sẽ có lợi thế nhiều hơn.
Ngoài ra các bạn cần có kiến thức cơ bản về lập trình như tin học đại cương, lập trình C hoặc Java

Bài 2: Cài đặt Flutter


Trong bài học này, mình sẽ hướng dẫn các bạn cài đặt Flutter framework trên máy tính cá nhân để chuẩn
bị môi trường học Flutter

Cài đặt Flutter trên Window


Các bước cài đặt Flutter SDK trên Windows
Bước 1 − Các bạn truy cập địa chỉ, https://flutter.dev/docs/get-started/install/windows và tải phiên bản
mới nhất của Flutter SDK. Hôm nay 08/04/2020 phiên bản mới nhất là 1.12.13 và file tải về là
flutter_windows_v1.12.13+hotfix.9-stable.zip
Bước 2 − Giải nén vào thư mục bất kỳ ví dụ C:\flutter\
Bước 3 − Cập nhật system path cho thư mục flutter\bin
Trong thanh tìm kiếm ở Start, bạn gõ ‘env’ sau đó chọn Edit environment variables for your account.. Dưới
dòng chữ User variables bạn kiểm tra nếu thấy ô Path:, thì thêm đường dẫn đầy đủ của thư mục flutter\bin
sử dụng dấu ; để ngăn cách với các biến khác.
Bước 4 − Flutter cung cấp một tool gọi là flutter doctor để kiểm tra tất cả những yêu cầu cần thiết cho
môi trường phát triển Flutter
flutter doctor
Bước 5 − Các bạn chạy lệnh phía trên để hệ thống kiểm tra và đưa ra báo cáo như sau
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, v1.2.1, on Microsoft Windows [Version
10.0.17134.706], locale en-US)
[√] Android toolchain - develop for Android devices (Android SDK version
28.0.3)
[√] Android Studio (version 3.2)
[√] VS Code, 64-bit edition (version 1.29.1)
[!] Connected device
! No devices available
! Doctor found issues in 1 category.
Như thông báo ở trên ta có thấy rằng Flutter SDK đã được cài, Android Tool đã được cài, Android Studio
đã được cài, chưa có kết nối tới thiết bị, bạn cần kết nối thiết bị điện thoại qua USB hoặc bật máy ảo
Bước 6 − Cài đặt bản Android SDK mới nhất nếu bạn chưa cài đặt và được cảnh báo bởi flutter doctor
Bước 7 − Cài đặt Android Studio mới nhất nếu bạn chưa cài đặt và được cảnh báo
Bước 8 − Bật android emulator hoặc kết nối tới một thiết bị Android
Bước 9 − Cài đặt plugin Flutter và Dart cho Android Studio. Hai plugin này sẽ cung cấp các template để
tạo ứng dụng Flutter và các tùy chọn để chạy và debug ứng dụng Flutter trên Android studio
1. Mở Android Studio.
2. Chọn File → Settings → Plugins.
3. Tìm kiếm Flutter plugin và click vào Install.
4. Chọn Yes khi hệ thống yêu cầu cài đặt Dart plugin.
5. Khởi động lại Android studio.

Cài đặt Flutter in MacOS


Để cài đặt Flutter SDK trên MacOS, các bạn thực hiện theo các bước sau
Bước 1 − Truy cập địa chỉ URL, https://flutter.dev/docs/get-started/install/macos và tải về phiên bản
Flutter SDK mới nhất
Bước 2 − Giải nén vào thư mục bất kỳ /path/to/flutter
Bước 3 − Cập nhật system path bao gồm thư mục flutter bin (ở trong ~/.bashrc file). bằng cách chạy lệnh
sau
> export PATH = "$PATH:/path/to/flutter/bin"
Bước 4 − Update lại hệ thống và kiểm tra Path bằng lệnh sau
source ~/.bashrc
source $HOME/.bash_profile
echo $PATH
Flutter cung cấp một tool, flutter doctor dùng để kiểm tra các yêu cầu cho Flutter tương tự bên Windows.
Bước 5 − Cài đặt bản mới nhất XCode nếu được yêu cầu bởi flutter doctor
Bước 6 − Cài đặt Android SDK nếu được yêu cầu bởi flutter doctor
Bước 7 − Cài đặt bản mới nhất Android Studio, nếu được yêu cầu bởi flutter doctor
Bước 8 − Bật máy ảo android emulator hoặc kết nối tới thiết bị Android nếu bạn phát triển ứng dụng
Android
Bước 9 − Bật iOS simulator hoặc kết nối tới thiết bị iPhone nếu bạn phát triển ứng dụng iOS
Bước 10 − Cài đặt Flutter và Dart plugin cho Android Studio tương tự như trên
Như vậy là mình đã hướng dẫn các bạn cài đặt Flutter SDK cho máy tính cá nhân, ở bài sau chúng ta sẽ
viết ứng dụng đầu tiên với Flutter

Bài 3: Tạo ứng dụng Flutter đầu tiên


Trong bài này, mình sẽ hướng dẫn các bạn tạo một Flutter Application đầu tiên trên Android Studio, quan
đó giúp các bạn hiểu cơ bản cấu trúc của một project ứng dụng Flutter
Bước 1 − Mở Android Studio
Bước 2 − Tạo Flutter Project mới: Chon Start a New Flutter Project hoặc từ menu File → New → New
Flutter Project
Bước 3 − Có nhiều loại project Flutter khác nhau chúng ta chọn Flutter Application và nhấn Next.
Bước 4 − Đặt tên và mô tả cho Project sau đó chọn Next.
Bạn đặt tên cho project là hello_app hoặc tên bất kì. Chọn đường dẫn thư mục Flutter SDK, nơi lưu trữ
project và mô tả của ứng dụng.
Bước 5 − Điền package_name cho ứng dụng.
Bước 6 − Nhấn Finish và đợi một lúc để Android Studio tiến hành việc tạo project
Sau khi tạo xong, chúng ta có thể thấy cấu trúc của một project Flutter như bên dưới.
Mình giải thích qua các thành phần của một Project Flutter:
● android − Thư mục code sinh tự động cho ứng dụng Android
● ios − Thư mục code sinh tự động cho ứng dụng iOS
● lib − Main folder chứa Dart code được viết khi sử dụng flutter framework
● ib/main.dart − File đầu tiên là điểm khởi đầu của ứng dụng Flutter application
● test − Folder chứa Dart code để test flutter application
● test/widget_test.dart − Sample code
● .gitignore − Git version control file - File này chứa cấu hình cho project git
● .metadata − sinh tự động bởi flutter tools
● .packages − sinh tự động để theo dõi flutter packages
● .iml − project file của Android studio
● pubspec.yaml − Được sử dụng Pub, Flutter package manager
● pubspec.lock − Sinh tự động bởi Flutter package manager, Pub
● README.md − Project description được viết theo cấu trúc Markdown
Bước 7 − Mặc định Android Studio đã tạo sẵn cho chúng ta code ở lib/main.dart file, tuy nhiên chúng ta
xoá đi và viết lại đoạn code dưới đây để dễ hiểu hơn
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: 'Hello World Demo Application',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: '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:
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

Bài 4: Kiến trúc ứng dụng Flutter


Trong bài học này, chúng ta sẽ tìm hiểu kiến trúc của ứng dụng Flutter (architecture of the Flutter
framework.)
Widgets
Khái niệm cốt lõi nhất trong Flutter framework đó là Trong Flutter, mọi thứ đều là widget. Widget (tiện ích)
là thành phần giao diện cơ bản nhất tạo nên toàn bộ giao diện người dùng của ứng dụng.
Trong Flutter, bản thân chính ứng dụng đã là một widget. Mỗi ứng dụng chính là một top-level widget và
nó bao gồm một hoặc nhiều các widget con, mỗi widget này lại có thể bao gồm một hoặc nhiều widget
con khác. Nhờ sự kết hợp linh hoạt này chúng ta có thể tạo ra bất kì ứng dụng phức tạp nào.
Ví dụ, chúng ta có thể nhìn vào cấu trúc widget của ứng dụng hello world (được học ở bài trước) thông
qua sơ đồ dưới đây:

Giải thích sơ đồ trên


● MyApp là một widget được tạo ra bằng widget gốc của Flutter, MaterialApp.
● MaterialApp có các thuộc tính của màn hình home và mô tả giao diện người dùng, nó lại được tạo
ra bởi một widget khác, MyHomePage.
● MyHomePage được tạo bởi một widget gốc của flutter, Scaffold
● Scaffold có 2 thuộc tính – body và appBar
● body chứa giao diện chính còn appBar chứa phần đầu (header) của ứng dụng
● Header UI là một widget gốc của flutter, AppBar và Body UI sử dụng Center widget.
● Center widget có một thuộc tính, Child, nó chứa phần nội dung chính là một Text widget
Gestures
Flutter widget hỗ trợ tương tác thông qua một widget đặc biệt gọi là GestureDetector. GestureDetector là
một tiện ích không hiển thị trên giao diện nhưng có khả năng nắm bắt các thao tác của người dùng như
nhấp, kéo, vuốt, chạm.... Phần lớn widget gốc của Flutter hỗ trợ tương tác giao diện thông qua
GestureDetector. Chúng ta sẽ tìm hiểu chi tiết về gesture (cử chỉ) trong các bài học tiếp theo.

Khái niệm State


Flutter widgets quản lý các State (trạng thái) thông qua một widget đặc biệt StatefulWidget. Tất cả các
Widget cần StatefulWidget để quản lý các trạng thái và kết nối với các widget khác. Flutter widgets là một
dạng reactive (kỹ thuật reactive hay reactive programming là gì thì nó hơi khó giải thích, các bạn tìm đọc
trên mạng nhé ) gốc. Nó tương tự như reactjs và StatefulWidget sẽ tự động thay đổi giao diện hiển thị khi
thay đổi trang thái. Sự thay đổi giao diện này được tối ưu bằng cách tìm kiếm sự khác biệt giữa UI widget
cũ và mới sau đó chỉ vẽ lại những thay đổi cần thiết.

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ụ.

Bài 5: Giới thiệu ngôn ngữ Dart


Dart là một ngôn ngữ lập trình mã nguồn mở (open source) đa năng (general purpose). Nó được phát
triển bởi Google. Dart là một ngôn ngữ lập trình hướng đối tượng sử dụng cú pháp của C ( C-style syntax).
Nó hỗ trợ các khái niệm như interface, class,... không giống như các ngôn ngữ lập trình khác, Dart không
hỗ trợ mảng (array). Dart collections có thể sử dụng các cấu trúc dữ liệu (data structure) thay thế.
Đoạn code dưới đây minh hoạ một chương trình Dart cơ bản:
void main() {
print("Dart language is easy to learn");
}

Biến và Kiểu dữ liệu


Biến (Variable) là tên đại diện cho nơi lưu trữ dữ liệu còn Kiểu dữ liệu (Data types) đơn giản là loại và kích
thước của dữ liệu liên kết với biến và hàm.
Dart sử dụng từ khóa var để khai báo biến.
var name = 'Dart';
Từ khóa final và const được sử dụng để khai báo hằng số (constants). Như ví dụ dưới đây
void main() {
final a = 12;
const pi = 3.14;
print(a);
print(pi);
}
Dart hỗ trợ các kiểu dữ liệu dưới đây, chúng ta không cần thiết phải khai báo kiểu dữ liệu cho biến
● Numbers − Được sử dụng cho số – Integer và Double.
● Strings − Được sử dụng cho chuỗi ký tự. Giá trị của String được đặt trong dấu nháy đơn hoặc
nháy kép.
● Booleans − Được sử dụng cho giá trị Boolean đúng và sai
● Lists and Maps − Được sử dụng cho nhóm đối tượng. Ví dụ một Danh sách đơn giản có thể được
mô tả như sau:
void main() {
var list = [1,2,3,4,5];
print(list);
}
Map có thể được mô tả như sau:
void main() {
var mapping = {'id': 1,'name':'Dart'};
print(mapping);
}
● Dynamic − Trường hợp kiểu dữ liệu chưa được định nghĩa thì giá trị mặc định là dynamic.
void main() {
dynamic name = "Dart";
print(name);
}

Điều khiển và vòng lặp


Một khối điều khiển (decision making block) đánh giá một điều kiện trước khi hướng dẫn thực thi. Dart hỗ
trợ các khối lệnh If, If..else và switch.
Vòng lặp được sử dụng để lặp lại một khối lệnh cho đến khi một điều kiện cụ thể được đáp ứng. Dart hỗ
trợ các vòng lặp for..in, while và do.. while
Ví dụ đoạn code hiển thị các số từ 1 đến 10
void main() {
for( var i = 1; i <= 10; i++ ) {
if(i%2==0) {
print(i);
}
}
}

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

Lập trình hướng đối tượng


Dart là một ngôn ngữ lập trình hướng đối tượng (object-oriented language). Nó hỗ trợ một số tính năng
của lập trình hướng đối tượng như class, interface,...
Mỗi một class (lớp) định nghĩa cho một loại đối tượng. Một class bao gồm những nội dung sau đây:
● Các thuộc tính (Fields)
● Các hàm Getter và setter
● Hàm khởi tạo (Constructor)
● Phương thức (Function)
Dưới đây là một minh hoạ các thành phần của một class
class Employee {
String name;

//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

Bài 6: Widget trong Flutter


Như đã nói trong các bài học trước widget là mọi thứ trong Flutter (widgets are everything in Flutter
framework). Đây là thành phần cơ bản và chủ yếu nhất. Nó tương tự như là các view ở trong Android.
Ở trong bài trước thì chúng ta đã tạo được các widget đơn giản, trong bài học này chúng ta sẽ tìm hiểu kĩ
hơn về cách tạo widget, cách thức hoạt động và các loại widget được hỗ trợ bởi Fullter.
Trong ứng dụng Hello World, chúng ta đã tạo một widget tên là MyHomePage
class MyHomePage extends StatelessWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;


@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(this.title), ),
body: Center(child: Text( 'Hello World',)),
);
}
}
Chúng ta sẽ tìm hiểu kỹ hơn về cách tạo widget này:
Đầu tiên chúng ta thấy rằng MyHomePage được kế thừa từ một widget khác là StatelessWidget. Thông
thường trong Flutter để tạo một widget mới (widget của người dùng nhé, ko phải widget mặc định) ta cần
kế thừa một trong hai class là StatelessWidget và StateFullWidget. Để hiểu hơn thì chúng ta sẽ nói rõ trong
bài Sate, nhưng chúng ta có thể hiểu đơn giản StatelessWidget là một widget không có trang thái nào, nó
chỉ nhận dữ liệu và hiển thị một cách thụ động, không nhận bất cứ event nào.
StatelessWidget chỉ yêu cầu implement duy nhất một phương thức build. Phương thức này lấy thông tin
để dựng các widget thông qua tham số BuildContext và trả về widget mà nó tạo ra.
Như đoạn code ở trên, ta có thể thấy hàm build sử dụng thuộc tính title từ hàm khởi tạo để hiển thị tiêu đề
cho ứng dụng. Còn tham số Key được dùng để định dạng cho widget.
Chúng ta thấy hàm build lại gọi tới một widget khác là Scaffold. Widget này đóng vai trò như một phần
nền để bố trí các thành phần khác theo phong cách Material Design, tương tự như DrawerLayout và
CoordinatorLayout trong Android vậy.
Cuối cùng ta thấy có một widget là Center có nhiệm vụ bố trí Text ở giữa màn hình.
Để hiểu hơn mối quan hệ giữa các widget chúng ta tham khảo sơ đồ dưới đây:
Tổng quan về các loại Widget trong Flutter
Trong Flutter tất các widget được phân loại dựa trên chức năng thành 4 nhóm sau:
● Các widget giao diện đặc thù theo từng nền tảng - Platform widgets
● Các widget hỗ trợ bố trí giao diện - Layout widgets
● Các widget quản lý trạng thái - State maintenance widgets
● Các widget cơ bản độc lập với nền tảng - Platform independent / basic widgets
Chúng ta sẽ tìm hiểu kỹ từng loại widget dưới đây:

Platform specific widgets


Đây là các widget dành riêng cho từng nền tảng Android hay IOS
Các widget dành riêng cho Android được thiết kế theo Material design guideline cho Android OS nên được
gọi là Material widgets.
Các widget dành riêng cho iOS được thiết kế theo Human Interface Guidelines bởi Apple và được gọi là
Cupertino widgets
Một số material widgets phổ biến nhất cho Android:
● Scaffold
● AppBar
● BottomNavigationBar
● TabBar
● TabBarView
● ListTile
● RaisedButton
● FloatingActionButton
● FlatButton
● IconButton
● DropdownButton
● PopupMenuButton
● ButtonBar
● TextField
● Checkbox
● Radio
● Switch
● Slider
● Date & Time Pickers
● SimpleDialog
● AlertDialog
Một số Cupertino widgets phổ biến nhất cho IOS
● CupertinoButton
● CupertinoPicker
● CupertinoDatePicker
● CupertinoTimerPicker
● CupertinoNavigationBar
● CupertinoTabBar
● CupertinoTabScaffold
● CupertinoTabView
● CupertinoTextField
● CupertinoDialog
● CupertinoDialogAction
● CupertinoFullscreenDialogTransition
● CupertinoPageScaffold
● CupertinoPageTransition
● CupertinoActionSheet
● CupertinoActivityIndicator
● CupertinoAlertDialog
● CupertinoPopupSurface
● CupertinoSlider

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.

State maintenance widgets


Trong Flutter, tất cả các widget đều kế thừa từ StatelessWidget hoặc StatefulWidget.
Widget kế thừa từ StatelessWidget sẽ không có bất kì trạng thái nào nhưng nó có thể bao gồm widget
được kế thừa từ StatefulWidget. Bản chất sự linh hoạt của ứng dụng là thông qua hành vi tương tác của
các widget và sự thay đổi trạng thái của chúng. Ví dụ khi chạm vào một nút tăng giảm của bộ đếm, nó sẽ
làm tăng hoặc giảm trạng thái của bộ đếm trong widget và cơ chế reactive nature (tự phản ứng) sẽ tự
động thay đổi lại giao diện của widget theo trạng thái mới.
Chúng ta sẽ tìm hiểu kỹ hơn về khái niệm StatefulWidget trong bài học về State management

Platform independent / basic widgets


Flutter cung cấp một số lượng lớn các widget cơ bản để tạo các giao diện người dùng từ đơn giản đến
phức tạp độc lập với nền tảng hệ điều hành. Chúng ta sẽ tìm hiểu một số widget cơ bản dưới đây:
Text
Text widget được sử dụng để hiển thị một đoạn văn bản. Chúng ta có thể định dạng văn bản thông qua
thuộc tính style vàTextStyle class. Ví dụ:
Text('Hello World!', style: TextStyle(fontWeight: FontWeight.bold))
Text widget có một hàm constructor riêng, Text.rich, sử dụng một TextSpan để mô tả các định dạng.
TextSpan widget có tính chất đệ quy và nó có thể bao gồm các TextSpan khác. Ví dụ:
Text.rich(
TextSpan(
children: [
TextSpan(text: "Hello ", style:
TextStyle(fontStyle: FontStyle.italic)),
TextSpan(text: "World", style:
TextStyle(fontWeight: FontWeight.bold)),
],
),
)
Một số thuộc tính cơ bản của Text widget
● maxLines, int − Số lượng dòng tối đa
● overflow, TextOverFlow − Xử lý việc tràn văn bản sử dụng TextOverFlow class
● style, TextStyle − Mô tả định dạng văn bản thông TextStyle class
● textAlign, TextAlign − Căn lề văn bản: right, left, justify,.. sử dụng TextAlign class
● textDirection, TextDirection − Quy định chiều của văn bản left-to-right hoặc right-to-left
Image
Image widget được sử dụng để hiển thị hình ảnh trong ứng dụng. Image widget cung cấp các phương
thức khởi tạo khác nhau để load hình ảnh từ các nguồn khác nhau:
● Image − Hình ảnh thông thường sử dụng ImageProvider
● Image.asset − Load hình ảnh từ flutter project’s assets
● Image.file − Load hình ảnh từ system folder
● Image.memory − Load hình ảnh từ memory
● Image.Network − Load hình ảnh từ mạng network
Lựa chọn đơn giản nhất để hiển thị hình ảnh trong Flutter là đưa hình ảnh vào thư mục assets của ứng
dụng rồi load vào widget khi cần:
● Tạo một thư mục assets ở trong project và copy file ảnh lưu vào.
● Mô tả assets ở trong file pubspec.yaml như sau
flutter:
assets:
- assets/smiley.png
Nhớ đúng cú pháp trên nhé
● Sau đó, load và hiển thị ảnh trong Ứng dụng bằng widget.
Image.asset('assets/smiley.png')
● Thay thế code MyHomePage widget trong ứng dụng hello world 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: 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

Bài 7: Layout trong Flutter


Trong Flutter các layout cũng là một loại widget, nhiệm vụ của chúng là bố trí các widget con, tạo nên
giao diện người dùng cho ứng dụng. Flutter cung cấp nhiều loại layout khác nhau như Container, Center,
Align... Chúng ta sẽ tìm hiểu chi tiết các loại layout trong bài học này.
Có hai loại widget layout chính trong Flutter
● Single Child Widgets - Chỉ có một widget con
● Multiple Child Widgets - Có nhiều widget con

Single Child Widgets


Các widget layout loại này chỉ có duy nhất một widget con và thường có chức năng bố trí nhất định.
Ví dụ, Center widget chỉ căn giữa widget con so với widget cha của nó và Container widget cung cấp khả
năng linh hoạt trong việc đặt widget con bên trong nó thông qua các tùy chọn như padding, đường viền,
nền,,,,
Single child widgets thích hợp cho việc tạo ra các widget có tính ứng dụng cao và chỉ có một chức năng
duy nhất như button, label....
Chúng ta sẽ xem thử đoạn code tạo ra một custom button sử dụng Container widget như sau:
class MyButton extends StatelessWidget {
MyButton({Key key}) : super(key: key);

@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:

Một số single child layout widgets quan trọng trong Flutter


● Padding − Được sử dụng để padding child widget. Ở đây, padding có thể sử dụng EdgeInsets
class.
● Align − Căn lề child widget sử dụng thuộc tính alignment. Giá trị của alignment có thể được cung
cấp bởi FractionalOffset class. FractionalOffset class xác định vị trí của phần tử từ vị trí điểm trên
cùng bên trái
Một số ví dụ về align
● FractionalOffset(1.0, 0.0) biểu thị phía trên bên phải
● FractionalOffset(0.0, 1.0) biểu thị phía dưới bên trái
Ví dụ qua đoạn code sau:
Center(
child: Container(
height: 100.0,
width: 100.0,
color: Colors.yellow, child: Align(
alignment: FractionalOffset(0.2, 0.6),
child: Container( height: 40.0, width:
40.0, color: Colors.red,
),
),
),
)

Một số single child layout khác:


● FittedBox
● AspectRatio
● ConstrainedBox
● Baseline
● FractinallySizedBox
● IntrinsicHeight
● IntrinsicWidth
● LimitedBox
● OffStage
● OverflowBox
● SizedBox
● SizedOverflowBox
● Transform
● CustomSingleChildLayout
Mình sẽ sửa lại một chút code MyHomePage trong ứng dụng Hello Word ở bài trước để các bạn hiểu rõ
hơn
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: 'Hello World Demo Application',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Home page'),
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(color: Colors.orange,),
padding: EdgeInsets.all(25), child: Center(
child:Text(
'Hello World', style: TextStyle(
color: Colors.red, letterSpacing: 0.5, fontSize: 30,
),
textDirection: TextDirection.ltr,
),
)
);
}
}
Chạy thử trên máy ảo
Multiple Child Widgets
Loại widget layout này sẽ cho phép có nhiều hơn một widget con. Ví dụ Row widget cho phép bố trí các
widget con theo chiều ngang thành một hàng trong khi Column widget cho phép bố trí các widget con
theo chiều dọc thành một cột.
Một số widget layout dạng này được sử dụng phổ biến
● Row
● Column
● ListView
● GridView
● Expanded
● Table
● Flow
● Stack

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());

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'),
);
}
}
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: 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:

Code của ProductBox như sau:


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: 120, 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()),
],
)
)
)
]
)
)
);
}
}
Giải thích:
ProductBox có 4 thuộc tính như sau:
● name - Tên sản phẩm
● description - Mô tả sản phẩm
● price - Giá của sản phẩm
● image - Hình ảnh cửa sản phẩm
ProductBox sử dụng 7 widget như sau:
● Container
● Expanded
● Row
● Column
● Card
● Text
● Image
Để hiểu hơn cấu trúc của ProductBox widget ta xem sơ đồ cấu trúc sau:

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'),
);
}
}

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"
),
],
)
);
}
}
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: 120, 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()),
],
)
)
)
]
)
)
);
}
}
Chạy thử ứng dụng trên máy ảo:
Bài 8: Gesture trong Flutter
Gesture (cử chỉ) là cách mà người dùng tương tác với các thiết bị di động. Có rất nhiều cử chỉ khác nhau
như vuốt, chạm, lắc... Gesture trong Flutter giúp ta xử lý các cử chỉ của người dùng, tương tự như việc bắt
sự kiện trong Android.
Một số cử chỉ được sử dụng rộng rãi:
● Tap − Chạm vào bề mặt thiết bị bằng đầu ngón tay trong thời gian ngắn sau đỏ thả ngón tay ra
ngay
● Double Tap − Tap 2 lần trong thời gian ngắn
● Drag − Chạm vào bề mặt của thiết bị bằng đầu ngón tay và sau đó di chuyển đầu ngón tay một
cách ổn định và cuối cùng thả ngón tay ra.
● Flick − Tương tự như drag nhưng thực hiện nhanh hơn.
● Pinch − Chụm bề mặt của thiết bị bằng hai ngón tay
● Spread/Zoom − Ngược lại với Pinch.
● Panning − Chạm vào bề mặt của thiết bị bằng đầu ngón tay và di chuyển nó theo bất kỳ hướng
nào mà không nhả đầu ngón tay.
Flutter cung cấp một sự hỗ trợ tuyệt vời để xử lý tất cả các loại cử chỉ thông qua một tiện ích duy nhất
GestureDetector. Để xác định các cử chỉ tác động lên một widget, ta chỉ cần đặt widget đó bên trong
GestureDetector widget. GestureDetector sẽ bắt các cử chỉ và gửi nhiều sự kiện dựa trên cử chỉ đó.
Một số cử chỉ và các sự kiện tương ứng được đưa ra dưới đây
Tap
● onTapDown
● onTapUp
● onTap
● onTapCancel
Double tap
● onDoubleTap
Long press
● onLongPress
Vertical drag
● onVerticalDragStart
● onVerticalDragUpdate
● onVerticalDragEnd
Horizontal drag
● onHorizontalDragStart
● onHorizontalDragUpdate
● onHorizontalDragEnd
Pan
● onPanStart
● onPanUpdate
● onPanEnd
Bây giờ chúng ta sẽ mở lại ứng dụng Hello world và thêm vào việc xử lý cử chỉ bằng việc sửa lại
MyHomePage widget như sau:
body: Center(
child: GestureDetector(
onTap: () {
_showDialog(context);
},
child: Text( 'Hello World', )
)
),
Nhìn vào đoạn code trên các bạn có thể thấy widget Text được đặt trong GestureDetector widget, để bắt
sự kiện onTap và hiển thị một Dialog khi ta chạm vào Text:
Ở hàm _showDialog ta sẽ gọi một widget là AlertDialog để hiển thị một thông báo, sử dụng đoạn code như
sau:
// user defined function void _showDialog(BuildContext context) {
// flutter defined function
showDialog(
context: context, builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
title: new Text("Message"),
content: new Text("Hello World"),
actions: <Widget>[
new FlatButton(
child: new Text("Close"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
Toàn bộ code của (main.dart) sau khi chỉnh sửa
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: 'Hello World Demo Application',
theme: ThemeData( primarySwatch: Colors.blue,),
home: MyHomePage(title: 'Home page'),
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;

// user defined function


void _showDialog(BuildContext context) {
// user defined function void _showDialog(BuildContext context) {
// flutter defined function
showDialog(
context: context, builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
title: new Text("Message"),
content: new Text("Hello World"),
actions: <Widget>[
new FlatButton(
child: new Text("Close"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}

@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

Bài 9: Quản lý trạng thái State trong Flutter


Quản lý state (trạng thái) của ứng dụng là một trong những bước quan trọng và cần thiết trong vòng đời
của một ứng dụng.
Chúng ta cùng xem xét một ứng dụng shopping cart (giỏ hàng) đơn giản dưới đây:
● Người dùng đăng nhập bằng thông tin của họ vào trong ứng dụng.
● Khi người dùng đăng nhập, ứng dụng sẽ hiển thị thông tin người dùng ở tất cả các màn hình
● Một lần nữa, khi người dùng chọn một sản phẩm và lưu vào trong giỏ hàng (cart). Thông tin giỏ
hàng sẽ tồn tại ở tất cả các trang cho đến khi người dùng xem giỏ hàng
● Thông tin người dùng và giỏ hàng tồn tại ở bất kì trường hợp nào gọi là trạng thái của ứng dụng ở
thời điểm đó
Việc quản lý trạng thái có thể được chia làm hai loại dựa vào thời gian tồn tại của trạng thái đó trong ứng
dụng:
● Ephemeral (ngắn hạn)− Kéo dài trong vài giây như trạng thái của hiệu ứng (animation) hoặc một
trang đơn như trang thông tin đánh giá sản phẩm. Flutter hỗ trợ quản lý trạng thái loại này thông
qua StatefulWidget.
● App state (trạng thái ứng dụng) − Kéo dài trong toàn bộ ứng dụng như thông tin người dùng,
thông tin giỏ hàng... Flutter hỗ trợ quản lý trạng thái loại này thông qua scoped_model
Ở những bài học sau, chúng ta sẽ tìm hiểu chi tiết về từng phương thức quản lý trạng thái. Sắp được một
nửa khoá học rồi. Cố lên!!!
Bài 10: Statefulwidget trong Flutter
Trong bài trước mình đã giới thiệu có hai phương thức quản lý trạng thái trong Flutter đó là quản lý trạng
thái ngắn hạn và quản lý trạng thái ứng dụng. Trong bài này chúng ta sẽ tìm hiểu quản lý trạng thái ngắn
hạn với Statefulwidget. Trong bài trước mình đã giới thiệu có hai phương thức quản lý trạng thái trong
Flutter đó là quản lý trạng thái ngắn hạn và quản lý trạng thái ứng dụng. Trong bài này chúng ta sẽ tìm
hiểu quản lý trạng thái ngắn hạn với Statefulwidget.
Vì ứng dụng Flutter được tạo nên từ các widget, do đó việc quản lý trạng thái cũng được thực hiện bởi
widget. Mấu chốt của việc quản lý trạng thái là Statefulwidget. Widget được kế thừa từ Statefulwidget để
duy trì trạng thái và quản lý các trạng thái con của nó.
Statefulwidget cung cấp tùy chọn cho widget để tạo ra các trạng thái, việc khởi tạo trạng thái ban đầu của
widget được thực hiện qua hàm createState và hàm setState dùng để thay đổi trạng thái khi cần. Sự thay
đổi trạng thái được thực hiện qua các gesture (cử chỉ). Ví dụ, sự đánh giá (rating) của một sản phẩm có
thể được thay đổi khi người dùng chạm vào sao của rating widget.
Bây giờ chúng ta sẽ tạo một widget là RatingBox để làm quen với quản lý trạng thái. Mục đích của widget
là hiển thị số lượng đánh giá hiện tại của sản phẩm.
Bước đầu tiên chúng ta tạo một widget là RatingBox kế thừa từ StatefulWidget.
class RatingBox extends StatefulWidget { }
Tạo một state cho RatingBox là _RatingBoxState kế thừa từ State<T>
class _RatingBoxState extends State<RatingBox> { }
Ghi đè phương thức createState của StatefulWidget để tạo trạng thái cho widget bằng _RatingBoxState
class RatingBox extends StatefulWidget {
@override
_RatingBoxState createState() => _RatingBoxState();
}
Tạo giao diện người dùng cho RatingBox widget bằng phương thức build của _RatingBoxState. Thông
thường chúng ta sẽ tạo giao diện bằng phương thức build của RatingBox widget. Tuy nhiên khi cần quản
lý trạng thái chúng ta cần tạo giao diện ở trong _RatingBoxState widget. Điều này đảm bảo cho việc hiển
thị lại giao diện người dùng khi trạng thái của widget thay đổi.
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],
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());

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'),
);
}
}

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"
),
],
)
);
}
}
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

Bài 11: ScopedModel trong Flutter


Trong bài trước chúng ta đã tìm hiểu cách quản lý trạng thái của widget bằng SatefullWidget, trong bài
hôm nay chúng ta tìm hiểu cách quản lý trạng thái trong ứng dụng bằng ScopedModel
Flutter cung cấp một phương pháp đơn giản để quản lý trạng thái của ứng dụng sử dụng scoped_model
package. Flutter package đơn giản là một thư viện với những phương thức được sử dụng nhiều lần. Chúng
ta sẽ tìm hiểu kỹ về package trong Flutter ở các bài học tới.
scoped_model cung cấp 3 class chính cho phép quản lý trạng thái của ứng dụng một cách mạnh mẽ:sdf
Model
Model đóng gói trạng thái của một ứng dụng. Chúng ta có thể sử dụng nhiều Model (bằng việc kế thừa
Model class) để quản lý trạng thái của ứng dụng. Model có một phương thức duy nhất là notifyListeners,
nó được gọi bất cứ khi nào trạng thái của Model thay đổi. notifyListeners sẽ thực hiện các công việc cần
thiết để cập nhật giao diện.
Ví dụ ta tạo một model là Product

class Product extends Model {


final String name;
final String description;
final int price;
final String image;
int rating;

Product(this.name, this.description, this.price, this.image, this.rating);


factory Product.fromMap(Map<String, dynamic> json) {
return Product(
json['name'],
json['description'],
json['price'],
json['image'],
json['rating'],
);
}
void updateRating(int myRating) {
rating = myRating; notifyListeners();
}
}

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());

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 state 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: 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;

Product(this.name, this.description, this.price, this.image, this.rating);


factory Product.fromMap(Map<String, dynamic> json) {
return Product(
json['name'],
json['description'],
json['price'],
json['image'],
json['rating'],
);
}
void updateRating(int myRating) {
rating = myRating;
notifyListeners();
}
}
Tốt rồi, giờ chúng ta sẽ sử dụng thuộc tính notifyListeners để lắng nghe sự thay đổi của UI khi mà người
dùng đánh giá
Chúng ta sẽ viết phương thức getProduct để tạo ra nội dung cho lớp Product
import product.dart in main.dart
import 'Product.dart';
static List<Product> getProducts() {
List<Product> items = <Product>[];

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;

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,
),
),
],
);
}
}

Ở đâ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;

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 có thể thấy chúng ta đã đóng gói widget RatingBox bên trong ScopedModel và
ScopedModelDecendant
Bây giờ chúng ta sẽ thay đổi widget MyHomePage để sử dụng widget ProductBox như sau:
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]);
},
)
);
}
}
Ở đâ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;

Product(this.name, this.description, this.price, this.image, this.rating);


factory Product.fromMap(Map<String, dynamic> json) {
return Product(
json['name'],
json['description'],
json['price'],
json['image'],
json['rating'],
);
} void cn "Laptop is most productive development tool", 2000, "laptop.jpg", 0));
items.add(
Product(
"Tablet"cnvn,
"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;
}
main.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'Product.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 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

Bài 12: Navigator và Routing


Như chúng ta đã biết, trong Flutter những gì chúng ta nhìn thấy được gọi là các widget. Navigator cũng là
một widget có chức năng quản lý các trang của ứng dụng theo định dạng giống như ngăn xếp. Trong bất
kì ứng dụng nào, việc điều hướng từ một Full-screen ( page/screen ) để làm một công việc xác định nào
đó ( chuyển sang một Full-screen khác) sử dụng Navigator widget thì được gọi là Routing. Flutter cung
cấp cho chúng ta lớp Routing cơ bản - MaterialPageRoute với hai phương thức - Navigator.push và
Navigator.pop. Hôm nay chúng ta hãy cùng nhau tìm hiểu kĩ về chức năng không thể thiểu trong lập trình
Flutter này nhé !

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());

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 state 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: Text('Hello World',)
),
);
}
}
Tiếp theo chúng ta sẽ tạo một lớp Product như sau :
class Product {
final String name;
final String description;
final int price;
final String image;
Product(this.name, this.description, this.price, this.image);
}
Tiếp đến chúng ta sẽ tạo phương thức getProduct từ lớp Product
import product.dart in main.dart
import 'Product.dart
static List<Product> getProducts() {
List<Product> items = <Product>[];

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;

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/" + 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(),
],
)
)
)
]
),
)
);
}
}
Tiếp tục, ta sẽ viết hàm có tên là MyHomePage widget để hiển thị toàn bộ danh sách Product và ta sẽ sử
dụng ListView.
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 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);

static List<Product> getProducts() {


List<Product> items = <Product>[];
items.add(
Product(
"Pixel",
"Pixel is the most featureful phone ever",
800,
"pixel.jpg"
)
);
items.add(
Product(
"Laptop",
"Laptop is most productive development tool",
2000,
"laptop.jpg"
)
);
items.add(
Product(
"Tablet",
"Tablet is the most useful device ever for meeting",
1500,
"tablet.jpg"
)
);
items.add(
Product(
"Pendrive",
"iPhone is the stylist phone ever",
100,
"pendrive.jpg"
)
);
items.add(
Product(
"Floppy Drive",
"iPhone is the stylist phone ever",
20,
"floppydrive.jpg"
)
);
items.add(
Product(
"iPhone",
"iPhone is the stylist phone ever",
1000,
"iphone.jpg"
)
);
return items;
}
}
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 Navigation 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 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;

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: 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(),
],
)
)
)
]
),
)
);
}
}
Sau khi hoàn thành, chúng ta sẽ run ứng dụng lên và click vào bất kì Product item nào, nó sẽ hiển thị các
trang chi tiết về sản phẩm liên quan và chúng ta có thể trở về màn hình HomePage bằng cách nhấn vào
nút quay lại. Dưới đây là hình ảnh để các bạn có thể dễ dàng quan sát hơn
Như vậy hôm nay chúng ta đã tìm hiểu cơ bản các khái niệm về Navigator, Routing và biết cách triển khai
nó. Cảm ơn các bạn đã theo dõi và chúc các bạn học tốt

Bài 13: Animation


Animation là một lớp trừu tượng và việc xử lý khá phức tạp. Nhưng thay vào đó, animation giúp nâng cao
trải nghiệm của người dùng, giúp người dùng tương tác với giao diện một cách thoải mái, hứng thú và
không gây nhàm chán. Chúng ta không thể phủ nhận tầm quan trọng của animation, một ứng dụng tuyệt
vời không chỉ là một ứng dụng chạy nhanh, nhẹ mà yếu tố quan trọng đó là giao diện phải đẹp, đơn giản
và hiệu ứng đa dạng.Flutter framework ghi nhận điều đó và đã cung cấp cho các lập trình viên các
framework đơn giản và trực quan để dễ dàng phát triển tất các dạng Animation.

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.

Bây giờ ta bắt đầu làm việc với Animation:

Đầ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>,
)

Làm việc với ứng dụng :


Bây giờ chúng ta sẽ tiến hành viết một ứng dụng đơn giản để hiểu về khái niệm animationtrong flutter
framework nhé ^^
Các bạn quay lại bài trước để lấy assets cũng như ảnh ở bài 7 nhé, mình sẽ để link ảnh dưới
đây(https://vncoder.vn/bai-hoc/layout-trong-flutter-225)
Mình tạo project với tên là flutter demo, sau đó xóa code mặc định, thêm import và hàm main cơ bản
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
Tạo MyApp widget kế thừa từ StateFullWidget( widget sẽ thay đổi giao diện khi được rebuild)
class MyApp extends StatefulWidget {
_MyAppState createState() => _MyAppState();
}
Tạo _MyAppState widget và implement initState, sau đó chúng ta thêm hàm dispose(đây là hàm đóng
controller)
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@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 _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@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();
}
}
Oke, mình cùng giải thích một tẹo về code đoạn code trên :
1. Hàm initState được dùng để tạo đối tượng animation controller (controller), đối tượng
animation(animation) và để bắt đầu với animation, chúng ta sử dụng phương thức
controller.forward.
2. Ở hàm dispose, như mình nói ở trên, sau khi tạo thì chúng ta phải hủy nó nên đó là chức năng của
hàm dispose để hủy bỏ controller
3. Ở hàm Build, animatio được gửi tới MyHomePage widget thông quan constructor. Bây giờ,
MyHomePage có thể sử dụng đối tượng animation tạo hiệu ứng cho nội dung
4. Giờ chúng ta sẽ tạo ProductBox widget
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()),
],
)
)
)
]
)
)
);
}
}
Tiếp theo chúng ta sẽ tạo widget mới có tên là MyAnimatedWidget với hiệu ứng đơn giản fade animation
sử dụng opacity(độ đục)
class MyAnimatedWidget extends StatelessWidget {
MyAnimatedWidget({this.child, this.animation});

final Widget child;


final Animation<double> animation;

Widget build(BuildContext context) => Center(


child: AnimatedBuilder(
animation: animation,
builder: (context, child) => Container(
child: Opacity(opacity: animation.value, child: child),
),
child: child),
);
}
Nhìn trên, chúng ta đã sử dụng AniatedBuilder để tạo hiệu ứng. AniatedBuilder là một widget được sử
dụng để build nội dung và hiệu ứng cùng một thời điểm. Nó cho phép đối tượng animation có thể lấy giá
trị của animation hiện tại. Chúng ta sử dụng giá trị animation thông qua animation.value để set opacity(độ
đục) của widget con. Và animation sẽ tác động lên child widget đang sử dụng opacity
Cuối cùng chúng ta sẽ tạo MyHomePage widget và sử dụng đối tượng animation để tạo hiệu ứng cho nội
dung của ứng dụng
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"
),
],
)
);
}
}

Ở đâ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());

class MyApp extends StatefulWidget {


_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;

@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;

Widget build(BuildContext context) => Center(


child: AnimatedBuilder(
animation: animation,
builder: (context, child) => Container(
child: Opacity(opacity: animation.value, child: child),
),
child: child
),
);
}
Sau khi hoàn thành và chạy ứng dụng thì ta sẽ có kết quả như dưới đây
Không quá khó phải không nào, hy vọng các bạn hiểu được bài hôm nay và có thể tự tạo animation riêng
cho mình.

Bài 14: Code với native Android


Flutter cung cấp framework chung để truy cập vào các nền tảng có tính năng riêng biệt. Việc này giúp cho
các lập trình viên có thể mở rộng các chức năng sử dụng nền tảng lập trình cụ thể như camera, pin, trình
duyệt web,...Có thể dễ dàng truy cập thông qua framework
Ý tưởng chung để truy cập vào mã cụ thể của nền tảng thông qua giao thức đơn giản là
messaging.Flutter code, Client, mã nền tảng và Host liên kết với một thông báo chung gọi là Message
Channel. Client sẽ gửi thông báo đến Host thông qua Message Channel. Host sẽ lắng nghe từ Message
Channel, nhận thông báo và xử lý các hàm cần thiết và cuối cùng trả kết quả về cho Clients thông qua
Message Channel.
Dưới đây là kiến trúc platform specific code được hiển thị thông qua sơ đồ khối :

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;

public class MainActivity extends FlutterActivity {


private static final String CHANNEL = "flutterapp.tutorialspoint.com/browser";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
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();
}
}
}
);
}
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);
}
}

main.dart
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.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;
static const platform = const
MethodChannel('flutterapp.tutorialspoint.com/browser');
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);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(this.title),
),
body: Center(
child: RaisedButton(
child: Text('Open Browser'),
onPressed: _openBrowser,
),
),
);
}
}
Giờ chúng ta chạy thử ứng dụng. Sau khi run ứng dụng, ta sẽ thấy nút Open browser như hình dưới, rồi ấn
vào nó
Bùm, và đây là kết quá.

Bài 15: Code với native IOS


Việc truy cập vào các nền tảng riêng của hệ điều hành IOS cũng giống như Android nhưng ta sẽ sử dụng
object C hay swift (ngôn ngữ dành riêng cho lập trình IOS) và IOS sdk. Tuy nhiên về khái niệm thì như
nhau
Nào, chúng ta sẽ bắt đầu viết một ứng dụng tương tự bài học trước nhưng sử dụng cho nền tảng IOS nhé
1. Tạo ứng dụng mới trên Android studio (enviroment) trên MacOS với tên "flutter_browser_ios_app"
2. Từ bước 2 đến bước 6 các bạn làm giống như bài 14( bài trước )
3. Sau đó các bạn khởi động Xcode, nhấn File->Open
4. Chọn Xcode project phía dưới ios director của flutter project
5. Mở AppDelegate.m dưới Runner -> Runner path. Và nó sẽ chứa dòng code sau
#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"
@implementation AppDelegate

- (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 {

// custom code starts


FlutterViewController* controller =
(FlutterViewController*)self.window.rootViewController;
FlutterMethodChannel* browserChannel = [
FlutterMethodChannel methodChannelWithName:
@"flutterapp.tutorialspoint.com /browser" binaryMessenger:controller];

__weak typeof(self) weakSelf = self;


[browserChannel setMethodCallHandler:^(
FlutterMethodCall* call, FlutterResult result) {

if ([@"openBrowser" isEqualToString:call.method]) {
NSString *url = call.arguments[@"url"];
[weakSelf openBrowser:url];
} else { result(FlutterMethodNotImplemented); }
}];
// custom code ends
[GeneratedPluginRegistrant registerWithRegistry:self];

// Override point for customization after application launch.


return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- (void)openBrowser:(NSString *)urlString {
NSURL *url = [NSURL URLWithString:urlString];
UIApplication *application = [UIApplication sharedApplication];
[application openURL:url];
}
@end

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.

Bài 16: Giới thiệu về package


Cách mà Dart tổ chức và chia sẻ các chức năng thông package. Dart Package là một thư viện hay mô
hình đơn giản có thể chia sẻ. Nhìn chung, Dart package cũng giống như Dart Application ngoại trừ Dart
Package không truy cập vào các điểm chính của ứng dụng
Cấu trúc chung của Package ( ví dụ về package) dưới đây :
- lib/src/* : tệp Dart ở dạng priavte
- lib/my_demo_package.dart : phần code chính của Dart, có thể thêm một vài ứng dụng
import 'package:my_demo_package/my_demo_package.dart'
- Một vài tệp ở dạng private có thể được xuất sang tệp chính (my_demo_package.dart) :
export src/my_private_code.dart
- lib/* : Ta có thể truy cập vào bất kì tệp nào bên trong thư mục :
import 'package:my_demo_package/custom_folder/custom_file.dart'
- pubspec.yaml : Được hiểu là trình quản lý thư mục của Package
Để tích hợp được các gói vào dự án thì ta cần phải có file pubspec.yaml

Các kiểu Package :

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

Sử dụng Dart Package :

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

Làm việc với Flutter plugin package


Làm việc với Flutter plugin cũng giống như làm việc với Dart package hay Dart application. Chỉ khác ở chỗ
Plugin sẽ sử dụng System API(Android hay iOS) để có được những chức năng cụ thể cần thiết
Như chúng ta đã được tìm hiểu cách truy cập vào các nền tảng riêng của hệ điều hành ở bài trước, hôm
nay chúng ta sẽ tự xây dựng một plugin đơn giản là my_browser. Chức năng của my_browser là cho phép
ứng dụng mở trình duyệt riêng của nền tảng ( IOS hay Android)
- Đầu tiên mở Android studio
- Nhấn file -> New flutter project và chọn futter plugin

- Nhập tên project "my_browser" và nhấn Next


- Nhập tên plugin và các thông tin khác như sau :
- Nhập tên miền công ty tên bất kì bạn muốn có dạng tenproject.tendomaincompany.com (không quan
trọng)
- Mở file my_browser.dart và viết các phương thức, openBrowser dùng để gọi phương thức trong nền tảng
riêng ( android hay ios )
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);
}
}
- Mở file MyBrowserPlugin.java và import một vài lớp như sau :
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
- Ở đây, chúng ta phải thêm thư viện cần thiết để mở browser từ Android
- Thêm các giá trị mRegistrar thuộc kiểu Registrar trong lớp MyBrowserPlugin ở dạng private
private final Registrar mRegistrar;
- Ở trên Registrar được dùng để lấy thông tin context của code được gọi
- Thêm constructor
private MyBrowserPlugin(Registrar registrar) {
this.mRegistrar = registrar;
}
- Thay đổi registerWith bao gồm constructor mới trong lớp MyBrowserPlugin
public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(),
"my_browser");
MyBrowserPlugin instance = new MyBrowserPlugin(registrar);
channel.setMethodCallHandler(instance);
}
- Thay đổi onMethodCall gồm hàm openBrowser trong lớp MyBrowserPlugin
@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();
}
}
- Sau đó viết hàm openBrowser để truy cập vào browser trong lớp MyBrowserPlugin
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);
}
Toàn bộ source code của my_browser plugin như sau :

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';

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: () => MyBrowser().openBrowser("https://flutter.dev"),
),
),
);
}
}
- Sau khi hoàn chỉnh ta sẽ chạy ứng dụng lên và click và nút open browser (hiển thị ở giữa màn hình) như
hình dưới đây
- Sau khi nhất nút open browser sẻ mở ra trình duyệt như ảnh dưới đây :
Bài hôm nay tương đối giống bài hôm trước.

Bài 17: REST API


Flutter cung cấp package http để sử dụng nguồn HTTP. http là một thư viện Future-based sử dụng tính
năng await và async. Nó cung cấp phương thức cấp cao và đơn giản để phát triển REST trên ứng dụng di
động.
Nội dung cơ bản :

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

Starting up http-server, serving .


Available on:
http://192.168.99.1:8000
http://127.0.0.1:8000
Hit CTRL-C to stop the server
Tạo file mới Product.dart trong lớp Product
Viết factory constructor trong lớp Product, Product.fromMap dùng để chuyển đổi dữ liệu map trong đối
tượng Product. Thông thường, tệp JSON sẽ được chuyển đổi bên trong đối tượng Dart Map và sau đó
chuyển đổi sang đối tượng liên quan (Product)
factory Product.fromJson(Map<String, dynamic> data) {
return Product(
data['name'],
data['description'],
data['price'],
data['image'],
);
}
Code trong hàm product.dart như sau :
class Product {
final String name;
final String description;
final int price;
final String image;

Product(this.name, this.description, this.price, this.image);


factory Product.fromMap(Map<String, dynamic> json) {
return Product(
json['name'],
json['description'],
json['price'],
json['image'],
);
}
}
Bây giờ ta sẽ viết 2 phương thức - parseProducts và fetchProducts - trong lớp chính để lấy và tải thông tin
sản phẩm từ web server(máy chủ) trong List<Product>
List<Product> parseProducts(String responseBody) {
final parsed = json.decode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<Product>((json) =>Product.fromJson(json)).toList();
}
Future<List<Product>> fetchProducts() async {
final response = await http.get('http://192.168.1.2:8000/products.json');
if (response.statusCode == 200) {
return parseProducts(response.body);
} else {
throw Exception('Unable to fetch products from the REST API');
}
}
Ta nên lưu ý một vài điểm sau :
-Future được sử dụng để trì hoãn việc tải thông tin sản phẩm cho đến khi cần thiết
- http.get được dùng để lấy dữ liệu từ internet
- json.decode được sử dụng để dịch dữ liệu JSON trong Dart Map. Mỗi một dữ liệu JSON được dịch, nó sẽ
chuyển vào List<Product> bằng fromMap của lớp Product
Trong lớp MyApp, thêm giá trị product mới, các product thuộc kiểu Future<Product> và đưa vào hàm
constructor
class MyApp extends StatelessWidget {
final Future<List<Product>> products;
MyApp({Key key, this.products}) : super(key: key);
...
Trong lớp MyHomePage, thêm một vài Product thuộc kiểu Future<Product> và đưa vào constructor.
class MyHomePage extends StatelessWidget {
final String title;
final Future<ListList<Product>> products;
MyHomePage({Key key, this.title, this.products}) : super(key: key);
...
Trong widget home (MyHomePage) thay đổi như sau
home: MyHomePage(title: 'Product Navigation demo home page', products: products),
Thay đổi hàm chính trên StateFullWidge
void main() => runApp(MyApp(fetchProduct()));
Tạo widget mới là ProductBoxList để xây dựng list Product trong home page
class ProductBoxList extends StatelessWidget {
final List<Product> items;
ProductBoxList({Key key, this.items});

@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)

// return the ListView widget :


Center(child: CircularProgressIndicator());
},
),
)
);
}
- Ở đây ta đã sử dụng widget FutureBuilder để render widget . FutureBuilder sẽ cố lấy dữ liệu từ thuộc tính
future (thuộc Future<List<Product>>). Nếu thuộc tính future trả dữ liệu về, nó sẽ render widget sử dụng
ProductBoxList, mặt khác sẽ ném lỗi(throw err)
Toàn bộ code trong hàm main.dart như dưới đây :
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'Product.dart';

void main() => runApp(MyApp(products: fetchProducts()));

List<Product> parseProducts(String responseBody) {


final parsed = json.decode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<Product>((json) => Product.fromMap(json)).toList();
}
Future<List<Product>> fetchProducts() async {
final response = await http.get('http://192.168.1.2:8000/products.json');
if (response.statusCode == 200) {
return parseProducts(response.body);
} else {
throw Exception('Unable to fetch products from the REST API');
}
}
class MyApp extends StatelessWidget {
final Future<List<Product>> products;
MyApp({Key key, this.products}) : super(key: key);

// 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 Navigation demo home page', products:
products),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
final Future<List<Product>> products;
MyHomePage({Key key, this.title, this.products}) : super(key: key);

// final items = Product.getProducts();


@override
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)

// return the ListView widget :


Center(child: CircularProgressIndicator());
},
),
)
);
}
}
class ProductBoxList extends StatelessWidget {
final List<Product> items;
ProductBoxList({Key key, this.items});

@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;

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/" + 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(),
],
)
)
)
]
),
)
);
}
}
OK, chạy ứng dụng và xem kết quả nhé. Nó sẽ cho ra cùng kết quả như ứng dụng Navigator(ở bài 12)
nhưng dữ liệu được lấy từ internet thay vì local. Chúc các bạn học tốt!

Bài 18: Khái niệm về Database


Flutter cung cấp một vài gói nâng cao để làm việc với database(cơ sở dữ liệu). Nhưng 2 gói quan trọng
nhất là :
1. sqflite - Sử dụng để truy vấn vào SQLlite database
2. firebase_database: Sử dụng để truy vấn và vận dụng đám mây lưu trữ NoSQL database từ
Google.
Trong chương này chúng ta sẽ bàn luận chi tiết về nó.

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");

List<Product> products = new List();


results.forEach((result) {
Product product = Product.fromMap(result);
products.add(product);
});
return products;
}
Ở trên, chúng ta đã làm như sau :
1. Sử dụng phương thức truy vấn để lấy tất cả thông tin của product. Truy vấn cung cấp lối tắt để
truy cập vào thông tin bảng mà không phải viết toàn bộ truy vấn . Phương thức truy vấn sẽ tạo
truy vấn chính nó bằng việc sử dụng đầu vào như columns, orderBy, ...
2. Sử dụng phương thức Product’s fromMap để lấy chi tiết product bằng việc chạy vòng lặp các đối
tượng
Chúng ta viết hàm để lấy id cụ thể của product
Future<Product> getProductById(int id) async {
final db = await database;
var result = await db.query("Product", where: "id = ", whereArgs: [id]);
return result.isNotEmpty ? Product.fromMap(result.first) : Null;
}
Ta đã sử dụng where và whereArgs để áp dụng bộ lọc
Tạo 3 hàm - insert, update và delete để thêm, cập nhật và xoá product từ db(database)
insert(Product product) async {
final db = await database;
var maxIdResult = await db.rawQuery(
"SELECT MAX(id)+1 as last_inserted_id FROM Product");

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;

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"]
);
}
);
}
Future<List<Product>> getAllProducts() async {
final db = await database;
List<Map> results = await db.query(
"Product", columns: Product.columns, orderBy: "id ASC"
);
List<Product> products = new List();
results.forEach((result) {
Product product = Product.fromMap(result);
products.add(product);
});
return products;
}
Future<Product> getProductById(int id) async {
final db = await database;
var result = await db.query("Product", where: "id = ", whereArgs: [id]);
return result.isNotEmpty ? Product.fromMap(result.first) : Null;
}
insert(Product product) async {
final db = await database;
var maxIdResult = await db.rawQuery("SELECT MAX(id)+1 as last_inserted_id FROM
Product");
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]);
}
}
Thay đổi trong hàm main để lấy thông tin product
void main() {
runApp(MyApp(products: SQLiteDbProvider.db.getAllProducts()));
}
Ở đây ta đã dùng getAllProducts để lấy tất cả sản phẩm từ db
Chạy ứng dụng lên và ta sẽ thấy được kết quả. Nó sẽ giống với kết quả ở ví dụ trước "REST API"

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;

Product(this.name, this.description, this.price, this.image);


factory Product.fromMap(Map<String, dynamic> json) {
return Product(
json['name'],
json['description'],
json['price'],
json['image'],
);
}
}
Copy thư mục assets từ product_rest_app vào product_firebase_app và thêm vào file 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 gói cloud_firestore trong tệp pubspec.yaml như sau :
dependencies: cloud_firestore: ^0.9.13+1
Ta đang sử dụng phiên bản mới nhất của cloud_firestore (ở thời điểm hiện tại)
Android studio sẽ thông báo cập nhật và Get dependencies nó.
Sau đó ta khởi tạo project Firebase theo từng bước sau đây :
1. Mở https://firebase.google.com/pricing/
2. Tạo một tài khoản Firebase và tạo mới Project
3. Các bạn làm theo hướng dẫn chi tiết trong video này nhé:
https://www.youtube.com/watch?v=6juww5Lmvgo
4. Lưu ý : bạn hãy nhớ kết nối project của mình với firebase nhé
Oke, ta hãy tạo cửa hàng sản phẩm mới như sau :
1. Đi đến Firebase console
2. Mở project mình vừa tạo
3. Nhấn vào tính năng Database trong menu bên trái
4. Tạo database
5. Nhấn Start trong test mode và Enable
6. Click Add collection.
Mở tệp main.dart và thêm plugin Cloud Firestore, xoá http package
import 'package:cloud_firestore/cloud_firestore.dart';
Xóa parseProducts và cập nhật fetchProducts để lấy products từ Cloud Firestore thay vì Product service
API.
Stream<QuerySnapshot> fetchProducts() {
return Firestore.instance.collection('product').snapshots(); }
Ở đây, phương thức Firestore.instance.collection được sử dụng để truy cập vào giá trị product trên cloud
store. Firestore.instance.collection cung cấp nhiều tính năng để lọc và lấy tài liệu cần thiết. Nhưng chúng
ta không áp dụng bất kỳ bộ lọc nào để lấy toàn bộ thông tin product
Cloud Firestore cung cấp bộ sưu tập thông qua khái niệm Dart Stream và sửa đổi kiểu dữ liệu products
trong widget MyApp và MyHomePage từ Future<List<Product>> sang Stream<QuerySnapshot>
Thay đổi phương thức build của widget MyHomePage để sử dụng StreamBuilder thay vì StreamBuilder
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Product Navigation")),
body: Center(
child: StreamBuilder<QuerySnapshot>(
stream: products, builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
if(snapshot.hasData) {
List<DocumentSnapshot>
documents = snapshot.data.documents;

List<Product>
items = List<Product>();

for(var i = 0; i < documents.length; i++) {


DocumentSnapshot document = documents[i];
items.add(Product.fromMap(document.data));
}
return ProductBoxList(items: items);
} else {
return Center(child: CircularProgressIndicator());
}
},
),
)
);
}
Ở trên, ta đã lấy được dữ liệu thông tin product như kiểu List<DocumentSnapshot>.
Cuối cùng, ta chạy ứng dụng và xem kết quả.

Bài 19: Chuyển đổi ngôn ngữ


Ngày nay, các ứng dụng di động được sử dụng từ nhiều nơi trên thế giới, vì thế các ứng dụng phải hiển thị
nội dung phù hợp với ngôn ngữ của quốc gia đó (ví dụ người Pháp thì nội dung hiển thị là tiếng Pháp,
người Việt thì hiển thị tiếng Việt).Việc ứng dụng làm việc với đa ngôn ngữ được gọi là
Internationalizing(quốc tế hoá).
Để ứng dụng làm việc với nhiều ngôn ngữ, đầu tiên nên tìm ngôn ngữ hiện tại của hệ thống mà ứng dụng
đang chạy và sau đó hiển thị nội dung ở vị trí cụ thể và quy trình này được gọi là Localization
Flutter framework cung cấp 3 lớp localization và các lớp tiện ích có nguồn gốc từ các lớp dựa trên localize
Các lớp cơ sở như sau :
Locale - là lớp được sử dụng để nhận diện ngôn ngữ người sử dụng. Ví dụ en-us nhận biết người Mỹ,
người Anh và nó được tạo ra như sau :
Locale en_locale = Locale('en', 'US')
Ở đây, đối số đầu tiên là mã ngôn ngữ, đối số thứ hai là mã quốc gia. Một ví dụ khác và tạo Argentina
Spanish (es-ar) như sau :
Locale es_locale = Locale('es', 'AR')
Localizations - là widget chung được sử dụng để set Locale và nguồn localized của lớp con
class CustomLocalizations {
CustomLocalizations(this.locale);
final Locale locale;
static CustomLocalizations of(BuildContext context) {
return Localizations.of<CustomLocalizations>(context, CustomLocalizations);
}
static Map<String, Map<String, String>> _resources = {
'en': {
'title': 'Demo',
'message': 'Hello World'
},
'es': {
'title': 'Manifestación',
'message': 'Hola Mundo',
},
};
String get title {
return _resources[locale.languageCode]['title'];
}
String get message {
return _resources[locale.languageCode]['message'];
}
}
Ở đây, CustomLocalizations là lớp custom mới được tạo riêng để lấy nội dung localized nhất định (tiêu đề
và thông báo) cho widget,of của phương thức sử dụng lớp Localizations để trả về lớp
CustomLocalizations mới
LocalizationsDelegate<T> - LocalizationsDelegate<T> là lớp factory thông qua widget Localizations được
tải. Nó có 3 phương thức over-ridable như sau :
* isSupported - Chấp nhận một miền - và trả về liệu miền đó có được hỗ trợ hay không?
@override
bool isSupported(Locale locale) => ['en', 'es'].contains(locale.languageCode);
Ở đây chỉ làm việc với en hoặc es
* load - Chấp nhận ngôn ngữ được chọn và bắt đầu tải các nguồn dữ liệu của ngôn ngữ đó
@override
Future<CustomLocalizations> load(Locale locale) {
return SynchronousFuture<CustomLocalizations>(CustomLocalizations(locale));
}
Ở trên, phương thức load trả về CustomLocalizations. việc trả về CustomLocalizations có thể được sử
dụng để lấy giá trị của tiêu đề và thông báo của cả 2 ngôn ngữ English và Spanish
* shouldReload - Liệu có nên tải lại CustomLocalizations là cần thiết khi widget Localizations được
rebuild(giống như việc reset lại trang)
@override
bool shouldReload(CustomLocalizationsDelegate old) => false;
Code ở CustomLocalizationDelegate như sau :
class CustomLocalizationsDelegate extends
LocalizationsDelegate<CustomLocalizations> {
const CustomLocalizationsDelegate();
@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;
}
Nhìn chung, ứng dụng Flutter dựa trên 2 cấp widget gốc là MaterialApp và WidgetsApp. Flutter cung cấp
miền cho cả 2 widget và nó là MaterialLocalizations và WidgetsLocaliations. Thêm nữa, flutter cũng cung
cấp quyền để tải MaterialLocalizations và WidgetsLocaliations, đó là
GlobalMaterialLocalizations.delegate và GlobalWidgetsLocalizations.delegate tương ứng. Chúng ta hãy
tạo ứng dụng đa ngôn ngữ cơ bản để chạy thử và hiểu về nội dung
Đầu tiên, ta tạo một ứng dụng mới với tên flutter_localization_app
Flutter hỗ trợ đa ngôn ngữ(internationalization) sử dụng gói flutter là flutter_localizations . Mở file
pubspec.yaml và thêm vào như sau :
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
Sau đó ta Get dependencies khi IDE hiện thông báo cập nhật
Thêm flutter_localizations vào main.dart:
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter/foundation.dart' show SynchronousFuture;
Ở đây, mục đích của SynchronousFuture để tải tuỳ chỉnh localizations không đồng bộ
Tạo custom localizations như sau :
class CustomLocalizations {
CustomLocalizations(this.locale);
final Locale locale;
static CustomLocalizations of(BuildContext context) {
return Localizations.of<CustomLocalizations>(context, CustomLocalizations);
}
static Map<String, Map<String, String>> _resources = {
'en': {
'title': 'Demo',
'message': 'Hello World'
},
'es': {
'title': 'Manifestación',
'message': 'Hola Mundo',
},
};
String get title {
return _resources[locale.languageCode]['title'];
}
String get message {
return _resources[locale.languageCode]['message'];
}
}
class CustomLocalizationsDelegate extends
LocalizationsDelegate<CustomLocalizations> {
const CustomLocalizationsDelegate();

@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.

Bài 20: Kiểm thử


Testing là một phần rất quan trọng trong việc phát triển vòng đời của một ứng dụng. Nó đảm bảo rằng
ứng dụng sẽ tốt hơn và chất lượng cao hơn.Testing yêu cầu có kế hoạch và thực thi cẩn thận. Nó cũng
tiêu tốn nhiều thời gian nhất trong việc lập trình
Dart và Flutter framework cung cấp gói mở rộng để hỗ trợ trong việc testing tự động của ứng dụng

Một số dạng Testing


Thông thường, có 3 loại testing

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>')

Các bước thực hiện


Widget Testing thực hiện 3 bước khác biệt như sau :
- Render widget trong môi trường testing
- WidgetTester là lớp cung cấp bởi Flutter testing framework để build và render widget. Phương thức
pumpWidget của lớp WidgetTester chấp nhận bất kỳ Widget và render nó trong môi trường testing
testWidgets('finds a specific instance', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Text('Hello'),
),
));
});
Tìm đến widget mà chúng ta cần test
- Flutter framework cung cấp nhiều tính năng để tìm đến widget render trong môi trường testing và gọi
chung là Finders.Chúng ta hầu như thường xuyên sử dụng sử dụng finders là find.text, find.byKey và
find.byWidget
1. find.text để tìm widget mà chứa đoạn text cụ thể - find.text('Hello')
2. find.byKey để tìm widget chứa các key cụ thể - find.byKey('home')
3. find.byWidget để tìm wiget theo biến thể của nó -find.byWidget (homeWidget)
- Đảm bảo các widget làm việc như mong đợi
- Flutter framework cung cấp nhiều tính năng để phù hợp widget với widget dự kiến và gọi là Matchers.
Một vài điều quan trọng về matchers như sau :
findsOneWidget - Xác minh widget duy nhất được tìm thấy
expect(find.text('Hello'), findsOneWidget);
findsNothing - Xác minh không wiget nào được tìm thấy
expect(find.text('Hello World'), findsNothing);
findsWidgets − Xác minh nhiều hơn một wiget được tìm thấy
expect(find.text('Save'), findsWidgets);
findsNWidgets - Xác minh N widget tìm thấy
expect(find.text('Save'), findsNWidgets(2));
Đoạn code ví dụ như sau :
testWidgets('finds hello widget', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Text('Hello'),
),
));
expect(find.text('Hello'), findsOneWidget);
});
Ở trên, chúng ta render một widget MaterialApp tới dòng text Hello sử dụng Text widget. Sau đó, ta sử
dụng find.text để tìm widget và khớp nó bằng cách sử dụng findsOneWidget
Ví dụ cụ thể
Ta sẽ tạo nhanh ứng dụng flutter và viết widget test để hiểu hơn các bước về ý tưởng với tên
flutter_test_app.
Mở widget_test.dart trong thư mục.
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());

// Verify that our counter starts at 0.


expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);

// Tap the '+' icon and trigger a frame.


await tester.tap(find.byIcon(Icons.add));
await tester.pump();

// Verify that our counter has incremented.


expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
Ở đây, test code có chức năng như sau :
- Renders MyApp widget sử dụng tester.pumpWidget
- Đảm bảo rằng bộ đếm ban đầu bằng 0 sử dụng findsOneWidget và findsOneWidget
- Tìm nút tăng bộ đếm sử dụng phương thức tester.tap
- Đảm bảo rằng bộ đếm được tăng lên bằng cách sử dụng findsOneWidget và findsNothing
Ta hãy thử nhấn vào nút tăng bộ đếm và sau đó kiểm tra liệu bộ đếm có tăng lên là 2
await tester.tap(find.byIcon(Icons.add));
await tester.pump();

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ả.

Bài 21: Xuất ứng dụng trong Flutter


Trong bài này mình sẽ giải thích làm thế nào để triển khai ứng dụng Flutter trên 2 nền tảng IOS và Android.

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

flutter build ios


- Và đây là output
Building com.example.MyApp for device (ios-release)...
Automatically signing iOS for device deployment
using specified development team in Xcode project:
Running Xcode build... 23.5s
......................

Bài 22: Công cụ phát triển


Hôm nay mình sẽ giải thích chi tiết về công cụ phát triển trong Flutter. Bộ công cụ phát triển đa nền tảng
đầu tiên được phát hành ngày 4/12/2018. Google đã tiếp tục làm việc để cải thiện và phát triển mạnh mẽ
flutter framework với các công cụ khác nhau

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.

Cài đặt DevTools


Để cài đặt Devtools thì ta cần nhấn lệnh sau :
flutter packages pub global activate devtools
Sau đó ở command line sẽ hiển thị :
Resolving dependencies...
+ args 1.5.1
+ async 2.2.0
+ charcode 1.1.2
+ codemirror 0.5.3+5.44.0
+ collection 1.14.11
+ convert 2.1.1
+ devtools 0.0.16
+ devtools_server 0.0.2
+ http 0.12.0+2
+ http_parser 3.1.3
+ intl 0.15.8
+ js 0.6.1+1
+ meta 1.1.7
+ mime 0.9.6+2
..................
..................
Installed executable devtools.
Activated devtools 0.0.16.

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

Bắt đầu với ứng dụng của bạn


Đi đến ứng dụng của bạn, mở giả lập và chạy với dòng lệnh :
flutter run --observatory-port=9200
Và bây giờ ta đã kết nối được với DevTools

Bắt đầu với DevTools trong Browser


Truy cập địa chỉ sau để kết nối với DevTools
http://localhost:9100/?port=9200
Trên trình duyệt của bạn sẽ hiển thị như sau :

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 packages upgrade


Bạn sẽ thấy dòng phản hồi
Running "flutter packages upgrade" in my_app... 7.4s

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;

List<Map> results = await db.query(


"Expense", columns: Expense.columns, orderBy: "date DESC"
);
List<Expense> expenses = new List();
results.forEach((result) {
Expense expense = Expense.fromMap(result);
expenses.add(expense);
});
return expenses;
}
● Lấy ra thông tin expense cụ thể dựa trên expense có sẵn trong db bằng phương thức
getExpenseById. Dùng để hiển thị thông tin cụ thể của một expense
Future<Expense> getExpenseById(int id) async {
final db = await database;
var result = await db.query("Expense", where: "id = ", whereArgs: [id]);

return result.isNotEmpty ?
Expense.fromMap(result.first) : Null;
}
● 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;

var result = await db.update("Expense", product.toMap(),


where: "id = ?", whereArgs: [product.id]);
return result;
}
● Xóa thông tin user có sẵn trong db bằng phương thức delete.
delete(int id) async {
final db = await database;
db.delete("Expense", where: "id = ?", whereArgs: [id]);
}
- Toàn bộ code trong SQLiteDbProvider 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 'Expense.dart';

class SQLiteDbProvider {
SQLiteDbProvider._();
static final SQLiteDbProvider db = SQLiteDbProvider._();

static Database _database; Future<Database> get database async {


if (_database != null)
return _database;
_database = await initDB();
return _database;
}
initDB() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, "ExpenseDB2.db");
return await openDatabase(
path, version: 1, onOpen:(db){}, onCreate: (Database db, int version) async {
await db.execute(
"CREATE TABLE Expense (
""id INTEGER PRIMARY KEY," "amount REAL," "date TEXT," "category
TEXT""
)
");
await db.execute(
"INSERT INTO Expense ('id', 'amount', 'date', 'category')
values (?, ?, ?, ?)",[1, 1000, '2019-04-01 10:00:00', "Food"]
);
/*await db.execute(
"INSERT INTO Product ('id', 'name', 'description', 'price', 'image')
values (?, ?, ?, ?, ?)", [
2, "Pixel", "Pixel is the most feature phone ever", 800, "pixel.png"
]
);
await db.execute(
"INSERT INTO Product ('id', 'name', 'description', 'price', 'image')
values (?, ?, ?, ?, ?)", [
3, "Laptop", "Laptop is most productive development tool", 2000,
"laptop.png"
]
);
await db.execute(
"INSERT INTO Product ('id', 'name', 'description', 'price', 'image')
values (?, ?, ?, ?, ?)", [
4, "Tablet", "Laptop is most productive development tool", 1500,
"tablet.png"
]
);
await db.execute(
"INSERT INTO Product ('id', 'name', 'description', 'price', 'image')
values (?, ?, ?, ?, ?)", [
5, "Pendrive", "iPhone is the stylist phone ever", 100,
"pendrive.png"
]
);
await db.execute(
"INSERT INTO Product ('id', 'name', 'description', 'price', 'image')
values (?, ?, ?, ?, ?)", [
6, "Floppy Drive", "iPhone is the stylist phone ever", 20,
"floppy.png"
]
); */
}
);
}
Future<List<Expense>> getAllExpenses() async {
final db = await database;
List<Map>
results = await db.query(
"Expense", columns: Expense.columns, orderBy: "date DESC"
);
List<Expense> expenses = new List();
results.forEach((result) {
Expense expense = Expense.fromMap(result);
expenses.add(expense);
});
return expenses;
}
Future<Expense> getExpenseById(int id) async {
final db = await database;
var result = await db.query("Expense", where: "id = ", whereArgs: [id]);
return result.isNotEmpty ? Expense.fromMap(result.first) : Null;
}
Future<double> getTotalExpense() async {
final db = await database;
List<Map> list = await db.rawQuery(
"Select SUM(amount) as amount from expense"
);
return list.isNotEmpty ? list[0]["amount"] : Null;
}
Future<Expense> insert(Expense expense) async {
final db = await database;
var maxIdResult = await db.rawQuery(
"SELECT MAX(id)+1 as last_inserted_id FROM Expense"
);
var id = maxIdResult.first["last_inserted_id"];
var result = await db.rawInsert(
"INSERT Into Expense (id, amount, date, category)"
" VALUES (?, ?, ?, ?)", [
id, expense.amount, expense.date.toString(), expense.category
]
);
return Expense(id, expense.amount, expense.date, expense.category);
}
update(Expense product) async {
final db = await database;
var result = await db.update(
"Expense", product.toMap(), where: "id = ?", whereArgs: [product.id]
);
return result;
}
delete(int id) async {
final db = await database;
db.delete("Expense", where: "id = ?", whereArgs: [id]);
}
}
- Ở đây ta giải thích như sau:
● database là thuộc tính để lấy đối tượng SQLiteDbProvider
● initDB là phương thức được sử dụng để chọn và mở SQLite db
- Tạo một file mới tên là ExpenseListModel.dart dùng để tạo ExpenseListModel, Mục đích của việc tạo
model này dùng để lưu giữ thông tin đầy đủ của user expense trong bộ nhớ và cập nhật giao diện user
trên ứng dụng bất cứ khi nào ta thay đổi user expense trong bộ nhớ. Nó hoạt động dựa vào lớp Model từ
package scoped_model. Nó có thuộc tính và phương thức như sau :
● _items : list expense ở dạng private (không truy cập trực tiếp vào biến này được)
● items - getter từ _items như UnmodifiableListView<expense> để ngăn chặn thay đổi bất ngờ hoặc
vô tình vào list
● totalExpense - getter Total expenses dựa trên các item có sẵn
double get totalExpense {
double amount = 0.0;
for(var i = 0; i < _items.length; i++) {
amount = amount + _items[i].amount;
}
return amount;
}
● load - Dùng để tải expense hoàn chỉnh từ db thông qua _items. Nó gọi phương thức
notifyListeners để cập nhật UI
void load() {
Future<List<Expense>>
list = SQLiteDbProvider.db.getAllExpenses();
list.then( (dbItems) {
for(var i = 0; i < dbItems.length; i++) {
_items.add(dbItems[i]);
} notifyListeners();
});
}
● byId - sử dụng để lấy expenses cụ thể từ _items
Expense byId(int id) {
for(var i = 0; i < _items.length; i++) {
if(_items[i].id == id) {
return _items[i];
}
}
return null;
}
● add - Sử dụng để thêm item expense mới thông qua _items vào db. Nó cũng gọi notifyListeners
để cập nhật UI
void add(Expense item) {
SQLiteDbProvider.db.insert(item).then((val) {
_items.add(val); notifyListeners();
});
}
● Update - Sử dụng để cập nhật item expense thông qua _items. Và gọi phương thức
notifyListeners để cập nhật UI
void update(Expense item) {
bool found = false;
for(var i = 0; i < _items.length; i++) {
if(_items[i].id == item.id) {
_items[i] = item;
found = true;
SQLiteDbProvider.db.update(item); break;
}
}
if(found) notifyListeners();
}
● delete - Sử dụng để xóa item expense thông qua biến _items từ db. Gọi notifyListeners để cập
nhật UI
void delete(Expense item) {
bool found = false;
for(var i = 0; i < _items.length; i++) {
if(_items[i].id == item.id) {
found = true;
SQLiteDbProvider.db.delete(item.id);
_items.removeAt(i); break;
}
}
if(found) notifyListeners();
}
- Toàn bộ code trong lớp ExpenseListModel :
import 'dart:collection';
import 'package:scoped_model/scoped_model.dart';
import 'Expense.dart';
import 'Database.dart';

class ExpenseListModel extends Model {


ExpenseListModel() {
this.load();
}
final List<Expense> _items = [];
UnmodifiableListView<Expense> get items =>
UnmodifiableListView(_items);

/*Future<double> get totalExpense {


return SQLiteDbProvider.db.getTotalExpense();
}*/

double get totalExpense {


double amount = 0.0;
for(var i = 0; i < _items.length; i++) {
amount = amount + _items[i].amount;
}
return amount;
}
void load() {
Future<List<Expense>> list = SQLiteDbProvider.db.getAllExpenses();
list.then( (dbItems) {
for(var i = 0; i < dbItems.length; i++) {
_items.add(dbItems[i]);
}
notifyListeners();
});
}
Expense byId(int id) {
for(var i = 0; i < _items.length; i++) {
if(_items[i].id == id) {
return _items[i];
}
}
return null;
}
void add(Expense item) {
SQLiteDbProvider.db.insert(item).then((val) {
_items.add(val);
notifyListeners();
});
}
void update(Expense item) {
bool found = false;
for(var i = 0; i < _items.length; i++) {
if(_items[i].id == item.id) {
_items[i] = item;
found = true;
SQLiteDbProvider.db.update(item);
break;
}
}
if(found) notifyListeners();
}
void delete(Expense item) {
bool found = false;
for(var i = 0; i < _items.length; i++) {
if(_items[i].id == item.id) {
found = true;
SQLiteDbProvider.db.delete(item.id);
_items.removeAt(i); break;
}
}
if(found) notifyListeners();
}
}
- Mở file main.dart, import một vài lớp như sau :
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'ExpenseListModel.dart';
import 'Expense.dart';
- Thêm hàm main và gọi runApp để đi tới widget ScopedModel<ExpenseListModel>
void main() {
final expenses = ExpenseListModel();
runApp(
ScopedModel<ExpenseListModel>(model: expenses, child: MyApp(),)
);
}
- Ở đây ta giải thích như sau
● expenses sẽ được tải toàn bộ dữ liệu thông tin của các user từ db. Ngoài ra, khi ứng dụng được
mở lần đầu, nó sẽ tự động tạo ra db cần thiết với thuộc tính bảng
● ScopedModel cung cấp thông tin expense trong toàn bộ vòng đời của ứng dụng và đảm bảo vùng
nhớ của ứng dụng trong bất kì trường hợp nào. Ta sẽ sử dụng StatelessWidget thay vì
StatefullWidget
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'),
);
}
}
- Tạo widget MyHomePage để hiển thị toàn bộ thông tin user expense
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), );
}
)
);
}
}
- Ta giải thích một vài điểm chính về đoạn code trên :
● ScopedModelDescendant được sử dụng để chuyển đến model expense trong Listview và widget
FloatingActionButton
● ListView.separated và widget ListTile được sử dụng để hiển thị thông tin expense
● widget Dismissible dùng để xoá bằng cử chỉ vuốt màn hình
● Navigator được sử dụng để mở giao diện. Nó có thể được kích hoạt bằng động tác chạm
Tạo widget FormPage. Mục đích của widget FormPage là thêm và cập nhật một expense
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'),
),
],
),
),
),
);
}
}
- Ở đâ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ả

You might also like