You are on page 1of 170

FLUTTER

Khoa CNTT
Tham khảo tại www.vncoder.vn
Mục lục
BÀI 1: GIỚI THIỆU FLUTTER - HỌC LẬP TRÌNH FLUTTER CƠ BẢN.............................................................1

BÀI 2: CÀI ĐẶT FLUTTER - HỌC LẬP TRÌNH FLUTTER CƠ BẢN..................................................................1

BÀI 3: TẠO ỨNG DỤNG FLUTTER ĐẦU TIÊN - HỌC LẬP TRÌNH FLUTTER CƠ BẢN.....................................3

BÀI 4: KIẾN TRÚC ỨNG DỤNG FLUTTER - HỌC LẬP TRÌNH FLUTTER CƠ BẢN...........................................8

BÀI 5: GIỚI THIỆU NGÔN NGỮ DART - HỌC LẬP TRÌNH FLUTTER CƠ BẢN.............................................10

BÀI 6: WIDGET TRONG FLUTTER - HỌC LẬP TRÌNH FLUTTER CƠ BẢN....................................................13

BÀI 7: LAYOUT TRONG FLUTTER - HỌC LẬP TRÌNH FLUTTER CƠ BẢN....................................................19

BÀI 8: GESTURE TRONG FLUTTER - HỌC LẬP TRÌNH FLUTTER CƠ BẢN..................................................30

BÀI 9: QUẢN LÝ TRẠNG THÁI SATE TRONG FLUTTER - HỌC LẬP TRÌNH FLUTTER CƠ BẢN.....................34

BÀI 10: STATEFULWIDGET TRONG FLUTTER - HỌC LẬP TRÌNH FLUTTER CƠ BẢN..................................35

BÀI 11: SCOPEDMODEL TRONG FLUTTER - HỌC LẬP TRÌNH FLUTTER CƠ BẢN......................................45

BÀI 12: NAVIGATOR VÀ ROUTING - HỌC LẬP TRÌNH FLUTTER CƠ BẢN.................................................57

BÀI 13: ANIMATION - HỌC LẬP TRÌNH FLUTTER CƠ BẢN......................................................................72

BÀI 14: CODE VỚI NATIVE ANDROID - HỌC LẬP TRÌNH FLUTTER CƠ BẢN.............................................85

BÀI 15: CODE VỚI NATIVE IOS - HỌC LẬP TRÌNH FLUTTER CƠ BẢN.......................................................93

BÀI 16: GIỚI THIỆU VỀ PACKAGE - HỌC LẬP TRÌNH FLUTTER CƠ BẢN...................................................96

BÀI 17: REST API - HỌC LẬP TRÌNH FLUTTER CƠ BẢN..........................................................................105

BÀI 18: KHÁI NIỆM VỀ DATABASE - HỌC LẬP TRÌNH FLUTTER CƠ BẢN...............................................117

BÀI 19: CHUYỂN ĐỔI NGÔN NGỮ - HỌC LẬP TRÌNH FLUTTER CƠ BẢN................................................128

BÀI 20: TESTING - HỌC LẬP TRÌNH FLUTTER CƠ BẢN..........................................................................136

BÀI 21: XUẤT ỨNG DỤNG TRONG FLUTTER - HỌC LẬP TRÌNH FLUTTER CƠ BẢN.................................139

BÀI 22: CÔNG CỤ PHÁT TRIỂN - HỌC LẬP TRÌNH FLUTTER CƠ BẢN.....................................................141

BÀI 23: VIẾT ỨNG DỤNG HOÀN CHỈNH - HỌC LẬP TRÌNH FLUTTER CƠ BẢN.......................................144
Tài liệu về Flutter
Flutter là gì?

Flutter là mobile UI framework của Google để tạo ra các giao diện native chất lượng cao trên iOS và
Android trong khoảng thời gian ngắn. Flutter hoạt động với source code có sẵn, được sử dụng bởi
các nhà phát triển và các tổ chức trên khắp thế giới, đồng thời nó open-source và miễn phí
Tại sao nên sử dụng Flutter?

Phát triển ứng dụng nhanh chóng: Tính năng hot reload của Flutter giúp bạn nhanh chóng và dễ
dàng thử nghiệm, xây dựng giao diện người dùng, thêm tính năng và sửa lỗi nhanh hơn. Trải
nghiệm tải lại lần thứ hai, mà không làm mất trạng thái, trên emulator, simulator và device cho iOS
và Android.
UI đẹp và biểu cảm: Thỏa mãn người dùng của bạn với các widget built-in đẹp mắt của Flutter theo
Material Design và Cupertino (iOS-flavor), các API chuyển động phong phú, scroll tự nhiên mượt mà
và tự nhận thức được nền tảng.
Framework hiện đại và reactive: Dễ dàng tạo giao diện người dùng của bạn với framework hiện đại,
reactive của Flutter và tập hợp các platform, layout và widget phong phú. Giải quyết các thách thức
giao diện người dùng khó khăn của bạn với các API mạnh mẽ và linh hoạt cho 2D, animation,
gesture, hiệu ứng và hơn thế nữa.
Truy cập các tính năng và SDK native: Làm cho ứng dụng của bạn trở nên sống động với API của
platform, SDK của bên thứ ba và native code. Flutter cho phép bạn sử dụng lại mã Java, Swift và
ObjC hiện tại của mình và truy cập các tính năng và SDK native trên iOS và Android.
Phát triển ứng dụng thống nhất: Flutter có các công cụ và thư viện để giúp bạn dễ dàng đưa ý tưởng
của mình vào cuộc sống trên iOS và Android. Nếu bạn chưa có kinh nghiệm phát triển trên thiết bị
di động, thì Flutter là một cách dễ dàng và nhanh chóng để xây dựng các ứng dụng di động tuyệt
đẹp. Nếu bạn là một nhà phát triển iOS hoặc Android có kinh nghiệm, bạn có thể sử dụng Flutter
cho các View của bạn và tận dụng nhiều code Java / Kotlin / ObjC / Swift hiện có của bạn.
Với những ưu điểm vượt trội có thể nói Flutter sẽ là tương lai sắp tới của lập trình di động. Đón đầu
xu hướng đó VnCoder biên soạn Khoá học lập trình di động với Flutter. Giúp các bạn lập trình viên
làm quen và sử dụng Flutter vào việc phát triển ứng dụng Android và iOS. Chúc các bạn học tập
chăm chỉ

ii
Bài 1: Giới thiệu Flutter - Học lập trình Flutter cơ bản
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 khoá 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 mobile. 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 bớ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) 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 trang 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

Hiện đã và là một react framework

 Sử dụng ngôn ngữ lập trình Dart đơn giản và dễ học


 Phát triển ứng dụng nhanh

1
 Giao diện người dùng rất đẹp và linh hoạt
 Hỗ trợ rất nhiều widget khác nhau
 Thể hiện cùng một UI trên nhiều nền tảng
 Ứ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ó độ tuỳ 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:

 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
 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.
 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.
 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
tuỳ biến và mở rộng lên càng mạnh mẽ hơn
 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
 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

2
Bài 2: Cài đặt Flutter - Học lập trình Flutter cơ bản
Cài đặt Flutter trên Window

Các bước cài đặt Flutter SDK trên Window

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ô 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 Stuido đã
đượ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 tuỳ chọn để chạy và debug ứng dụng Flutter trên Android studio

Mở Android Studio.

Chọn File → Settings → Plugins.


Tìm kiếm  Flutter plugin và click vào Install.

Chọn Yes khi hệ thống yêu cầu cài đặt Dart plugin.

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ô 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 Fullter 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
Bài 3: Tạo ứng dụng Flutter đầu tiên - Học lập trình Flutter cơ bả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 proect 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ụngtheme 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 aỏ 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


Bài 4: Kiến trúc ứng dụng Flutter - Học lập trình Flutter cơ bản
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 tí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 lữ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 - Học lập trình Flutter cơ bản

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 GoogleI. 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ốn như các ngôn ngữ lập tì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à Kể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ừ khoá var để khai báo biến. 

var name = 'Dart';


Từ khoá 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 đinh 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 6: Widget trong Flutter - Học lập trình Flutter cơ bản
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ết 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ị têu đề
cho ứng dụng. Còn tham số Key được dùng để định dang 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ết 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  FloatingActionButton  Checkbox


 AppBar  FlatButton  Radio
 BottomNavigationBar  IconButton  Switch
 TabBar  DropdownButton  Slider
 TabBarView  PopupMenuButton  Date & Time Pickers
 ListTile  ButtonBar  SimpleDialog
 RaisedButton  TextField  AlertDialog
Một số Cupertino widgets phổ biến nhất cho IOS

 CupertinoButton  CupertinoTimerPicker  CupertinoTabScaffold


 CupertinoPicker  CupertinoNavigationBar  CupertinoTabView
 CupertinoDatePicker  CupertinoTabBar  CupertinoTextField
 CupertinoDialog  CupertinoPageTransition  CupertinoPopupSurface
 CupertinoDialogAction  CupertinoActionSheet  CupertinoSlider
 CupertinoFullscreenDialo  CupertinoActivityIndicat
gTransition or
 CupertinoPageScaffold  CupertinoAlertDialog
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 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ẽ tim 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ết 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ứ
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
dang. 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 dang 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 đinh 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 7: Layout trong Flutter - Học lập trình Flutter cơ bản
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 tuỳ 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ị phiá 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  FractinallySizedBox  OffStage


 AspectRatio  IntrinsicHeight  OverflowBox
 ConstrainedBox  IntrinsicWidth  SizedBox
 Baseline  LiimitedBox  SizedOverflowBox
 Transform  CustomSingleChildLa
yout
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   GridView   Flow 


 Column   Expanded   Stack 
 ListView   Table 
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ế tuỳ
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 anhr 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 - Học lập trình Flutter cơ bản
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  Long press  onHorizontalDragStart


 onTapDown  onLongPress  onHorizontalDragUpdate
 onTapUp Vertical drag  onHorizontalDragEnd
 onTap Pan
 onVerticalDragStart
 onTapCancel
 onVerticalDragUpdate  onPanStart
Double tap  onVerticalDragEnd  onPanUpdate
 onDoubleTap Horizontal drag  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 Sate trong Flutter - Học lập trình Flutter cơ bản
Quản lý sate (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 gía 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 - Học lập trình Flutter cơ bản

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
StatefulwidgetTrong 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 đo sviệ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ết 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 tuỳ chọn cho widget để tạo ra các trạng thái, việc khởi tạo trang 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 trang 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
Bài 11: ScopedModel trong Flutter - Học lập trình Flutter cơ bản
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ẽ:

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

Content :ScopedModelDescendant chuyển sang context của ứng dụng

Child : Một phần của UI và không thay đổi dựa trên Data Model

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 :

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

Sau đó thay thế các dòng code mặc định trong hàm main.dart bằng product_state_app code nhé

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'],
);n
} 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);
}
)
],
)
)
)
)
]
),
)
);
}
}
Bài 12: Navigator và Routing - Học lập trình Flutter cơ bản

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ác 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ẽ xử
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 - Học lập trình Flutter cơ bản
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 :

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

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

Thời gian khi chạy animation có tác động đến tốc độ (nhanh hay chậm) của hiệu ứng

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 dể diề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 nghia 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();
}
}
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.

Ở 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

Ở 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

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. Chúc các bạn thành
công ^^
Bài 14: Code với native Android - Học lập trình Flutter cơ bản
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ơ đồi 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 channe 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ài 15: Code với native IOS - Học lập trình Flutter cơ bản
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
 Mở project setting
 Tìm đến Capabilities bật Background Modes.
 Thêm *Background fetch và Remote Notification**
 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é   
Bài 16: Giới thiệu về package - Học lập trình Flutter cơ bản
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 dung

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ệng í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ện 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 17: REST API - Học lập trình Flutter cơ bản

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 qua (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(),
],
)
)
)
]
),
)
);
}
}
Bài 18: Khái niệm về Database - Học lập trình Flutter cơ bản
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à:

 sqflite - Sử dụng để truy vấn vào SQLlite database


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

Tạo/mở SQLite database

Thực thi SQL statement (thực thi phương thức) đối với SQLite database 

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 :

async được sử dụng để viết phương thức asynchronous(không đồng bộ)

io được sử dụng để truy cập vào tệp và thư mục

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

path_provider được sử dụng để lấy đường dẫn tạm thời 

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 :

 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 , ...
 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 .
 Tạo ừng dụng flutter mới tên là product_firebase_app
 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 :
Mở https://firebase.google.com/pricing/
Tạo một tài khoảng Firebase và tạo mới Project 
 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
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 :
Đi đến Firebase console
Mở project mình vừa tạo
Nhấn vào tính năng Database trong menu bên trái
Tạo database
Nhấn Start trong test mode và EnaEnable
Click Add collection. 

Mở tệp main.dart và thêm plugin Cloud Firestore , xoá http package

import 'package:cloud_firestore/cloud_firestore.dart';
Xoá 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, p[hươ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ả . Chúng ta đã sử dụng cùng thông tin product trong SQLite
application và chỉ thay đổi nơi lưu trữ vì thế ứng dụng sẽ hiển thị giống như SQLite application . Chúc các bạn
học tốt ^^
Bài 19: Chuyển đổi ngôn ngữ - Học lập trình Flutter cơ bản
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ườ 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 :

* sSupported - 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ữ Englist 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 mobile.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 . 

Intl.canonicalizedLocale - được sử dụng để lấy chính xác tên ngôn ngữ 

Intl.defaultLocale  - Sử dụng để set ngôn ngữ hiện tại

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: Testing - Học lập trình Flutter cơ bản
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

find.text để tìm widget mà chứa đoạn text cụ thể - find.text('Hello')

find.byKey để tìm widget chứa các key cụ thể - find.byKey('home')

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 - Học lập trình Flutter cơ bản

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 TIẾP THEO: CÔNG CỤ PHÁT TRIỂN >>
Bài 22: Công cụ phát triển - Học lập trình Flutter cơ bả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/22018. Google đã tiếp tục làm việc để cả 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 :

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

Ctrl+ : công cụ sửa lỗi nhanh 

Phím tắt khi coding

Cung cấp chi tiết chức năng và cách dùng trong comments

Phím tắt Debugging

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 - Học lập trình Flutter cơ bản
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 :

Danh sách chi phí

Tạo mẫu để nhập chi phí mới

Tính năng chỉnh sửa/xoá 

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

Sử dụng listview để hiển thị ra danh sách chi phí

Lập trình mẫu

Sử dụng SQLite để lưu trữ chi phí

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ử dung để 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;
}
Xoá 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 modell
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 để xoá 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ử run ứ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