Professional Documents
Culture Documents
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 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 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 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 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 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.
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
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
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 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 Yes khi hệ thống yêu cầu cài đặt Dart plugin.
Để 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 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
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 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ạ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
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
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:
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.
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 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");
}
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
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
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
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
Đầ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:
Đâ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
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.
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 đổ).
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).
Chúng ta sẽ tim hiểu kỹ hơn trong bài Giới thiệu layout widget ở các bài sau.
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
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.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
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:
Tạo một thư mục assets ở trong project và copy file ảnh lưu vào.
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:
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á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:
@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:
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
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:
import 'package:flutter/material.dart';
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.
Để 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());
@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:
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:
@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());
@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;
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
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:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(this.title),),
body: Center(
child: 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:
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.
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.
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)
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.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title:Text("Product Listing")),
body: ListView(
shrinkWrap: true, padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0),
children: <Widget> [
ProductBox(
name: "iPhone",
description: "iPhone is the stylist phone ever",
price: 1000,
image: "iphone.jpg"
),
ProductBox(
name: "Pixel",
description: "Pixel is the most featureful phone ever",
price: 800,
image: "pixel.jpg"
),
ProductBox(
name: "Laptop",
description: "Laptop is most productive development tool",
price: 2000,
image: "laptop.jpg"
),
ProductBox(
name: "Tablet",
description: "Tablet is the most useful device ever for meeting",
price: 1500,
image: "tablet.jpg"
),
ProductBox(
name: "Pendrive",
description: "Pendrive is useful storage medium",
price: 100,
image: "pendrive.jpg"
),
ProductBox(
name: "Floppy Drive",
description: "Floppy drive is useful rescue storage medium",
price: 20,
image: "floppydisk.jpg"
),
],
)
);
}
}
class RatingBox extends StatefulWidget {
@override
_RatingBoxState createState() =>
_RatingBoxState();
}
class _RatingBoxState extends State<RatingBox> {
int _rating = 0;
void _setRatingAsOne() {
setState( () {
_rating = 1;
});
}
void _setRatingAsTwo() {
setState( () {
_rating = 2;
});
}
void _setRatingAsThree() {
setState( () {
_rating = 3;
});
}
Widget build(BuildContext context) {
double _size = 20;
print(_rating);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 1 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
onPressed: _setRatingAsOne,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 2 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
onPressed: _setRatingAsTwo,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_rating >= 3 ? Icon(Icons.star, size: _size,) :
Icon(Icons.star_border, size: _size,)),
color: Colors.red[500],
onPressed: _setRatingAsThree,
iconSize: _size,
),
),
],
);
}
}
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.name, this.description, this.price, this.image}) :
super(key: key);
final String name;
final String description;
final int price;
final String image;
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(2),
height: 140,
child: Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Image.asset("assets/appimages/" + image),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(this.name, style: TextStyle(fontWeight: FontWeight.bold)),
Text(this.description),
Text("Price: " + this.price.toString()),
RatingBox(),
],
)
)
)
]
)
)
);
}
}
Các bạn chạy thử ứng dụng, và chạm vào các ngôi sao để xem trạng thái của RatingBox thay đổi
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.
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ố:
Child : Một phần của UI và không thay đổi dựa trên Data Model
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ây giờ bạn hãy thay thế đoạn code mặc định(main.dart) bằng đoạn code của chúng tôi nhé
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(this.title),
),
body: Center(
child: Text( 'Hello World', )
),
);
}
}
Bạn nhớ import Scope_model vào main.dart nhé ^^
import 'package:scoped_model/scoped_model.dart';
Giờ chúng ta sẽ tạo lớp Product quen thuộc, Product.dart là lớp chứa thông tin của sản phẩm Product
import 'package:scoped_model/scoped_model.dart';
class Product extends Model {
final String name;
final String description;
final int price;
final String image;
int rating;
Chúng ta sẽ viết phương thức getProduct để tạo ra nội dung cho lớp 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é
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;
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;
@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
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 :
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 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
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.
@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é.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(this.item.name),
),
body: Center(
child: Container(
padding: EdgeInsets.all(0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.asset("assets/appimages/" + this.item.image),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(
this.item.name, style: TextStyle(
fontWeight: FontWeight.bold
)
),
Text(this.item.description),
Text("Price: " + this.item.price.toString()),
RatingBox(),
],
)
)
)
]
),
),
),
);
}
}
Và chúng ta đã hoàn thành, dưới đây là toàn bộ code trong ứng dụng.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class Product {
final String name;
final String description;
final int price;
final String image;
Product(this.name, this.description, this.price, this.image);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Product Navigation")),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return GestureDetector(
child: ProductBox(item: items[index]),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductPage(item: items[index]),
),
);
},
);
},
)
);
}
}
class ProductPage extends StatelessWidget {
ProductPage({Key key, this.item}) : super(key: key);
final Product item;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(this.item.name),
),
body: Center(
child: Container(
padding: EdgeInsets.all(0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.asset("assets/" + this.item.image,height: 100.0,width: MediaQuery.of(context).size.width,),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(this.item.name, style: TextStyle(fontWeight: FontWeight.bold)),
Text(this.item.description),
Text("Price: " + this.item.price.toString()),
RatingBox(),
],
)
)
)
]
),
),
),
);
}
}
class RatingBox extends StatefulWidget {
@override
_RatingBoxState createState() => _RatingBoxState();
}
class _RatingBoxState extends State<RatingBox> {
int _rating = 0;
void _setRatingAsOne() {
setState(() {
_rating = 1;
});
}
void _setRatingAsTwo() {
setState(() {
_rating = 2;
});
}
void _setRatingAsThree() {
setState(() {
_rating = 3;
});
}
Widget build(BuildContext context) {
double _size = 20;
print(_rating);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 1 ? Icon(
Icons.star,
size: _size,
)
: Icon(
Icons.star_border,
size: _size,
)
),
color: Colors.red[500],
onPressed: _setRatingAsOne,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 2 ?
Icon(
Icons.star,
size: _size,
)
: Icon(
Icons.star_border,
size: _size,
)
),
color: Colors.red[500],
onPressed: _setRatingAsTwo,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 3 ?
Icon(
Icons.star,
size: _size,
)
: Icon(
Icons.star_border,
size: _size,
)
),
color: Colors.red[500],
onPressed: _setRatingAsThree,
iconSize: _size,
),
),
],
);
}
}
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.item}) : super(key: key);
final Product item;
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à:
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
Ở đâ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 :
Đượ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
-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)
Ở 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
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
@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
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 10), vsync: this);
animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
controller.forward();
}
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
controller.forward();
return MaterialApp(
title: 'Flutter Demo', theme: ThemeData(primarySwatch: Colors.blue,),
home: MyHomePage(title: 'Product layout demo home page', animation:
animation,)
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key key, this.title, this.animation}): super(key: key);
final String title;
final Animation<double> animation;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Product Listing")),
body: ListView(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0),
children: <Widget>[
FadeTransition(
child: ProductBox(
name: "iPhone",
description: "iPhone is the stylist phone ever",
price: 1000,
image: "iphone.jpg"
),
opacity: animation
),
MyAnimatedWidget(
child: ProductBox(
name: "Pixel",
description: "Pixel is the most featureful phone ever",
price: 800,
image: "pixel.jpg"
),
animation: animation
),
ProductBox(
name: "Laptop",
description: "Laptop is most productive development tool",
price: 2000,
image: "laptop.jpg"
),
ProductBox(
name: "Tablet",
description: "Tablet is the most useful device ever for meeting",
price: 1500,
image: "tablet.jpg"
),
ProductBox(
name: "Pendrive",
description: "Pendrive is useful storage medium",
price: 100,
image: "pendrive.jpg"
),
ProductBox(
name: "Floppy Drive",
description: "Floppy drive is useful rescue storage medium",
price: 20,
image: "floppydrive.jpg"
),
],
)
);
}
}
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.name, this.description, this.price, this.image}) :
super(key: key);
final String name;
final String description;
final int price;
final String image;
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(2),
height: 140,
child: Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Image.asset("assets/" + image),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(
this.name, style: TextStyle(
fontWeight: FontWeight.bold
)
),
Text(this.description), Text(
"Price: " + this.price.toString()
),
],
)
)
)
]
)
)
);
}
}
class MyAnimatedWidget extends StatelessWidget {
MyAnimatedWidget({this.child, this.animation});
final Widget child;
final Animation<double> animation;
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é
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
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ể
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 ;
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;
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;
FlutterMethodChannel* browserChannel = [
FlutterMethodChannel methodChannelWithName:
@"flutterapp.tutorialspoint.com/browser" binaryMessenger:controller];
Tạo biến weakSelf và đặt class hiện tại
Bây giờ ta sẽ implement setMethodCallHandler. Gọi hàm openBrowser bởi call.method. Lấy giá trị url bằng
call.arguments và bỏ qua nó khi gọi openBrowser
[browserChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
if ([@"openBrowser" isEqualToString:call.method]) {
NSString *url = call.arguments[@"url"];
[weakSelf openBrowser:url];
} else { result(FlutterMethodNotImplemented); }
}];
Dưới đây là toàn bộ code mẫu, mời bạn đọc cùng tham khảo
#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if ([@"openBrowser" isEqualToString:call.method]) {
NSString *url = call.arguments[@"url"];
[weakSelf openBrowser:url];
} else { result(FlutterMethodNotImplemented); }
}];
// custom code ends
[GeneratedPluginRegistrant registerWithRegistry:self];
- 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
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 :
- 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 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)
- 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 )
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
- Thêm constructor
@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
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
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 :
import 'package:flutter/material.dart';
import 'package:my_browser/my_browser.dart';
- 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.
- 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
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
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
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
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 :
cd /path/to/JSONWebServer
- Cài đặt http-server package bằng cách sử dụng npm
http-server . -p 8000
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;
- 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
@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
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'Product.dart';
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return GestureDetector(
child: ProductBox(item: items[index]),
onTap: () {
Navigator.push(
context, MaterialPageRoute(
builder: (context) => ProductPage(item: items[index]),
),
);
},
);
},
);
}
}
class ProductPage extends StatelessWidget {
ProductPage({Key key, this.item}) : super(key: key);
final Product item;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(this.item.name),),
body: Center(
child: Container(
padding: EdgeInsets.all(0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.asset("assets/appimages/" + this.item.image),
Expanded(
child: Container(
padding: EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(this.item.name, style:
TextStyle(fontWeight: FontWeight.bold)),
Text(this.item.description),
Text("Price: " + this.item.price.toString()),
RatingBox(),
],
)
)
)
]
),
),
),
);
}
}
class RatingBox extends StatefulWidget {
@override
_RatingBoxState createState() =>_RatingBoxState();
}
class _RatingBoxState extends State<RatingBox> {
int _rating = 0;
void _setRatingAsOne() {
setState(() {
_rating = 1;
});
}
void _setRatingAsTwo() {
setState(() {
_rating = 2;
});
}
void _setRatingAsThree() {
setState(() {
_rating = 3;
});
}
Widget build(BuildContext context) {
double _size = 20;
print(_rating);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 1
? Icon(Icons.star, ize: _size,)
: Icon(Icons.star_border, size: _size,)
),
color: Colors.red[500], onPressed: _setRatingAsOne, iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 2
? Icon(Icons.star, size: _size,)
: Icon(Icons.star_border, size: _size, )
),
color: Colors.red[500],
onPressed: _setRatingAsTwo,
iconSize: _size,
),
),
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (
_rating >= 3 ?
Icon(Icons.star, size: _size,)
: Icon(Icons.star_border, size: _size,)
),
color: Colors.red[500],
onPressed: _setRatingAsThree,
iconSize: _size,
),
),
],
);
}
}
class ProductBox extends StatelessWidget {
ProductBox({Key key, this.item}) : super(key: key);
final Product item;
SQLite
SQLite là một SQL tiêu chuẩn dựa trên công cụ cơ sở dữ liệu nhúng . Nó là công cụ nhỏ và đang được thử
nghiệm theo thời gian. Gói sqflite cung cấp nhiều chức năng để làm việc hiệu quả với SQLite database. Nó
cung cấp các phương thức tiêu chuẩn để vận hành SQLite database. Chức năng chính của sqflite như sau :
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.
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
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
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 :
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
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
join : sử dụng để tạo đường dẫn cụ thể. Ta đã sử dụng nó để tạo đường dẫn database
onCreate : Sử dụng để viết code khi database được tạo lần đầu
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
Tạo 3 hàm - insert, update và delete để thêm, cập nhật và xoá product từ db(database)
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;
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;
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.
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>();
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
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 :
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
* 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ộ
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
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 :
Biên dịch và chạy ứng dụng . Ứng dụng sẽ hiển thị nội dung là tiếng Anh.
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
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
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 '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 .
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 :
{
"@@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.
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
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
- 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
- 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
- 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 :
expect(find.text('Hello'), findsOneWidget);
findsNothing - Xác minh không wiget nào đượ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 :
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());
- Đả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 :
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 :
- Đầ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
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
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
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.
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
Đi đến ứng dụng của bạn, mở giả lập và chạy với dòng lệnh :
http://localhost:9100/?port=9200
Trên trình duyệt của bạn sẽ hiển thị như sau :
Flutter SDK
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
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 quản lý vùng nhớ scoped_model để đơn giản hoá việc lập trình
- Tạo một ứng dụng flutter với tên expense_calculator trên android studio hoặc visual studio code tuỳ ý
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 :
path_provider được sử dụng để lấy đường dẫn hệ thống riêng của ứng dụng
-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: category - Dùng để phân biệt các đối tượng đã chi tiêu như : Food, Travel, ..
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
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
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 đó
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._();
_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
import 'dart:collection';
import 'package:scoped_model/scoped_model.dart';
import 'Expense.dart';
import 'Database.dart';
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
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;
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 :
Hàm _submit được sử dụng cùng với expense để thêm hoặc cập nhật expenses trong db
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 "+"