1. Giới thiệu
Flutter là bộ công cụ giao diện người dùng của Google để xây dựng các ứng dụng cho thiết bị di động, web và máy tính chỉ từ một bộ mã cơ sở. Trong lớp học lập trình này, bạn sẽ tạo ứng dụng Flutter sau đây:
Ứng dụng này tạo ra những tên nghe hay, chẳng hạn như "newstay", "lightstream", "mainbrake" hoặc "graypine". Người dùng có thể yêu cầu tên tiếp theo, thêm tên hiện tại vào danh sách yêu thích và xem danh sách tên yêu thích trên một trang riêng. Ứng dụng có khả năng thích ứng với nhiều kích thước màn hình.
Kiến thức bạn sẽ học được
- Kiến thức cơ bản về cách hoạt động của Flutter
- Tạo bố cục trong Flutter
- Kết nối các lượt tương tác của người dùng (chẳng hạn như lượt nhấn nút) với hành vi của ứng dụng
- Sắp xếp mã Flutter gọn gàng
- Giúp ứng dụng của bạn thích ứng (với nhiều màn hình)
- Đạt được giao diện nhất quán cho ứng dụng của bạn
Bạn sẽ bắt đầu bằng một cấu trúc cơ bản để có thể chuyển thẳng đến các phần thú vị.
Và đây là Filip hướng dẫn bạn thực hiện toàn bộ lớp học lập trình!
Nhấp vào nút tiếp theo để bắt đầu phòng thí nghiệm.
2. Thiết lập môi trường Flutter
Người chỉnh sửa
Để lớp học lập trình này diễn ra suôn sẻ nhất có thể, chúng tôi giả định rằng bạn sẽ sử dụng Visual Studio Code (VS Code) làm môi trường phát triển. Ứng dụng này miễn phí và hoạt động trên tất cả các nền tảng chính.
Tất nhiên, bạn có thể sử dụng bất kỳ trình chỉnh sửa nào mà bạn thích: Android Studio, các IDE IntelliJ khác, Emacs, Vim hoặc Notepad++. Tất cả đều hoạt động với Flutter.
Bạn nên sử dụng VS Code cho lớp học lập trình này vì hướng dẫn mặc định là các phím tắt dành riêng cho VS Code. Bạn nên nói những câu như "nhấp vào đây" hoặc "nhấn phím này" thay vì những câu như "thực hiện hành động thích hợp trong trình chỉnh sửa để làm X".
Chọn mục tiêu phát triển
Flutter là một bộ công cụ đa nền tảng. Ứng dụng của bạn có thể chạy trên bất kỳ hệ điều hành nào sau đây:
- iOS
- Android
- Windows
- macOS
- Linux
- web
Tuy nhiên, thông thường, bạn nên chọn một hệ điều hành duy nhất mà bạn sẽ chủ yếu phát triển. Đây là "mục tiêu phát triển" của bạn – hệ điều hành mà ứng dụng của bạn chạy trong quá trình phát triển.
Ví dụ: giả sử bạn đang dùng máy tính xách tay Windows để phát triển một ứng dụng Flutter. Nếu chọn Android làm mục tiêu phát triển, bạn thường kết nối một thiết bị Android với máy tính xách tay Windows bằng cáp USB và ứng dụng đang phát triển của bạn sẽ chạy trên thiết bị Android được kết nối đó. Tuy nhiên, bạn cũng có thể chọn Windows làm mục tiêu phát triển. Điều này có nghĩa là ứng dụng đang phát triển của bạn sẽ chạy dưới dạng một ứng dụng Windows cùng với trình chỉnh sửa.
Bạn có thể muốn chọn web làm mục tiêu phát triển. Nhược điểm của lựa chọn này là bạn sẽ mất một trong những tính năng phát triển hữu ích nhất của Flutter: Tải lại nóng có trạng thái. Flutter không thể tải lại nhanh các ứng dụng web.
Hãy chọn ngay. Lưu ý: Bạn luôn có thể chạy ứng dụng của mình trên các hệ điều hành khác sau này. Chỉ là việc xác định rõ mục tiêu phát triển sẽ giúp bước tiếp theo diễn ra suôn sẻ hơn.
Cài đặt Flutter
Hướng dẫn mới nhất về cách cài đặt Flutter SDK luôn có tại docs.flutter.dev.
Hướng dẫn trên trang web Flutter không chỉ đề cập đến việc cài đặt SDK mà còn đề cập đến các công cụ liên quan đến mục tiêu phát triển và các trình bổ trợ của trình chỉnh sửa. Xin lưu ý rằng đối với lớp học lập trình này, bạn chỉ cần cài đặt những thành phần sau:
- Flutter SDK
- Visual Studio Code có trình bổ trợ Flutter
- Phần mềm mà mục tiêu phát triển bạn chọn yêu cầu (ví dụ: Visual Studio để nhắm đến Windows hoặc Xcode để nhắm đến macOS)
Trong phần tiếp theo, bạn sẽ tạo dự án Flutter đầu tiên.
Nếu gặp vấn đề cho đến thời điểm này, bạn có thể thấy một số câu hỏi và câu trả lời (trên StackOverflow) sau đây hữu ích cho việc khắc phục sự cố.
Câu hỏi thường gặp
- Làm cách nào để tìm đường dẫn của Flutter SDK?
- Tôi phải làm gì khi không tìm thấy lệnh Flutter?
- Làm cách nào để khắc phục vấn đề "Đang chờ một lệnh flutter khác giải phóng khoá khởi động"?
- Làm cách nào để cho Flutter biết vị trí cài đặt SDK Android của tôi?
- Làm cách nào để xử lý lỗi Java khi chạy
flutter doctor --android-licenses
? - Làm cách nào để xử lý lỗi không tìm thấy công cụ
sdkmanager
Android? - Làm cách nào để xử lý lỗi "Thiếu thành phần
cmdline-tools
"? - Làm cách nào để chạy CocoaPods trên Apple Silicon (M1)?
- Làm cách nào để tắt tính năng tự động định dạng khi lưu trong VS Code?
3. Tạo một dự án
Tạo dự án Flutter đầu tiên
Khởi chạy Visual Studio Code và mở bảng lệnh (bằng F1
hoặc Ctrl+Shift+P
hoặc Shift+Cmd+P
). Bắt đầu nhập "flutter new". Chọn lệnh Flutter: New Project (Flutter: Dự án mới).
Tiếp theo, hãy chọn Application (Ứng dụng) rồi chọn một thư mục để tạo dự án. Đây có thể là thư mục chính của bạn hoặc một thư mục nào đó như C:\src\
.
Cuối cùng, hãy đặt tên cho dự án của bạn. Chẳng hạn như namer_app
hoặc my_awesome_namer
.
Giờ đây, Flutter sẽ tạo thư mục dự án và VS Code sẽ mở thư mục đó.
Giờ đây, bạn sẽ ghi đè nội dung của 3 tệp bằng một cấu trúc cơ bản của ứng dụng.
Sao chép và dán ứng dụng ban đầu
Trong ngăn bên trái của VS Code, hãy nhớ chọn Explorer (Trình khám phá) rồi mở tệp pubspec.yaml
.
Thay thế nội dung của tệp này bằng nội dung sau:
pubspec.yaml
name: namer_app
description: "A new Flutter project."
publish_to: "none"
version: 0.1.0
environment:
sdk: ^3.9.0
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.1.5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
Tệp pubspec.yaml
chỉ định thông tin cơ bản về ứng dụng của bạn, chẳng hạn như phiên bản hiện tại, các phần phụ thuộc và những thành phần mà ứng dụng sẽ đi kèm.
Tiếp theo, hãy mở một tệp cấu hình khác trong dự án, analysis_options.yaml
.
Thay thế nội dung của tệp bằng nội dung sau:
analysis_options.yaml
include: package:flutter_lints/flutter.yaml
linter:
rules:
avoid_print: false
prefer_const_constructors_in_immutables: false
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
prefer_final_fields: false
unnecessary_breaks: true
use_key_in_widget_constructors: false
Tệp này xác định mức độ nghiêm ngặt mà Flutter cần tuân thủ khi phân tích mã của bạn. Vì đây là lần đầu tiên bạn tìm hiểu về Flutter, nên bạn đang yêu cầu trình phân tích hoạt động ở mức độ cơ bản. Bạn luôn có thể điều chỉnh chế độ này sau. Trên thực tế, khi tiến gần đến việc xuất bản một ứng dụng sản xuất thực tế, bạn gần như chắc chắn sẽ muốn trình phân tích nghiêm ngặt hơn thế này.
Cuối cùng, hãy mở tệp main.dart
trong thư mục lib/
.
Thay thế nội dung của tệp này bằng nội dung sau:
lib/main.dart
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
return Scaffold(
body: Column(
children: [Text('A random idea:'), Text(appState.current.asLowerCase)],
),
);
}
}
50 dòng mã này là toàn bộ mã nguồn của ứng dụng cho đến thời điểm hiện tại.
Trong phần tiếp theo, hãy chạy ứng dụng ở chế độ gỡ lỗi và bắt đầu phát triển.
4. Thêm nút
Bước này sẽ thêm nút Tiếp theo để tạo một cặp từ mới.
Chạy ứng dụng
Trước tiên, hãy mở lib/main.dart
và đảm bảo bạn đã chọn thiết bị đích. Ở góc dưới cùng bên phải của VS Code, bạn sẽ thấy một nút cho biết thiết bị mục tiêu hiện tại. Nhấp để thay đổi.
Trong khi lib/main.dart
đang mở, hãy tìm nút "phát" ở góc trên bên phải cửa sổ VS Code rồi nhấp vào nút đó.
Sau khoảng một phút, ứng dụng của bạn sẽ khởi chạy ở chế độ gỡ lỗi. Hiện tại, bạn chưa thấy gì nhiều:
First Hot Reload
Ở cuối lib/main.dart
, hãy thêm nội dung vào chuỗi trong đối tượng Text
đầu tiên và lưu tệp (bằng Ctrl+S
hoặc Cmd+S
). Ví dụ:
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'), // ← Example change.
Text(appState.current.asLowerCase),
],
),
);
// ...
Lưu ý cách ứng dụng thay đổi ngay lập tức nhưng từ ngẫu nhiên vẫn giữ nguyên. Đây là tính năng Tải lại nhanh có trạng thái nổi tiếng của Flutter. Tính năng tải lại nhanh sẽ được kích hoạt khi bạn lưu các thay đổi vào một tệp nguồn.
Câu hỏi thường gặp
- Điều gì sẽ xảy ra nếu tính năng Tải lại nhanh không hoạt động trong VSCode?
- Tôi có phải nhấn "r" để tải lại nhanh trong VSCode không?
- Tính năng Tải lại nóng có hoạt động trên web không?
- Làm cách nào để xoá biểu ngữ "Gỡ lỗi"?
Thêm một nút
Tiếp theo, hãy thêm một nút ở cuối Column
, ngay bên dưới phiên bản Text
thứ hai.
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(appState.current.asLowerCase),
// ↓ Add this.
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
],
),
);
// ...
Khi bạn lưu thay đổi, ứng dụng sẽ cập nhật lại: Một nút sẽ xuất hiện và khi bạn nhấp vào nút đó, Bảng điều khiển gỡ lỗi trong VS Code sẽ hiện thông báo đã nhấn nút!.
Khoá học cấp tốc về Flutter trong 5 phút
Mặc dù Bảng điều khiển gỡ lỗi rất thú vị, nhưng bạn muốn nút này làm được điều gì đó có ý nghĩa hơn. Tuy nhiên, trước khi làm việc đó, hãy xem xét kỹ hơn mã trong lib/main.dart
để hiểu cách hoạt động của mã.
lib/main.dart
// ...
void main() {
runApp(MyApp());
}
// ...
Ở đầu tệp, bạn sẽ thấy hàm main()
. Ở dạng hiện tại, tệp này chỉ yêu cầu Flutter chạy ứng dụng được xác định trong MyApp
.
lib/main.dart
// ...
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
// ...
Lớp MyApp
mở rộng StatelessWidget
. Tiện ích là những phần tử mà bạn dùng để tạo mọi ứng dụng Flutter. Như bạn thấy, ngay cả bản thân ứng dụng cũng là một tiện ích.
Mã trong MyApp
thiết lập toàn bộ ứng dụng. Mã này tạo trạng thái trên toàn ứng dụng (sẽ nói thêm về vấn đề này sau), đặt tên cho ứng dụng, xác định giao diện trực quan và đặt tiện ích "home" (trang chủ) – điểm bắt đầu của ứng dụng.
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
// ...
Tiếp theo, lớp MyAppState
xác định trạng thái của ứng dụng. Đây là lần đầu tiên bạn khám phá Flutter, vì vậy lớp học lập trình này sẽ giữ cho mọi thứ đơn giản và tập trung. Có nhiều cách hiệu quả để quản lý trạng thái ứng dụng trong Flutter. Một trong những cách dễ giải thích nhất là ChangeNotifier
, phương pháp mà ứng dụng này áp dụng.
MyAppState
xác định dữ liệu mà ứng dụng cần để hoạt động. Hiện tại, nó chỉ chứa một biến duy nhất với cặp từ ngẫu nhiên hiện tại. Bạn sẽ bổ sung thêm vào danh sách này sau.- Lớp trạng thái mở rộng
ChangeNotifier
, nghĩa là lớp này có thể thông báo cho những lớp khác về các thay đổi của chính nó. Ví dụ: nếu cặp từ hiện tại thay đổi, một số tiện ích trong ứng dụng cần biết. - Trạng thái được tạo và cung cấp cho toàn bộ ứng dụng bằng cách sử dụng
ChangeNotifierProvider
(xem mã ở trên trongMyApp
). Điều này cho phép mọi tiện ích trong ứng dụng nắm bắt được trạng thái.
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) { // ← 1
var appState = context.watch<MyAppState>(); // ← 2
return Scaffold( // ← 3
body: Column( // ← 4
children: [
Text('A random AWESOME idea:'), // ← 5
Text(appState.current.asLowerCase), // ← 6
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
], // ← 7
),
);
}
}
// ...
Cuối cùng là MyHomePage
, tiện ích mà bạn đã sửa đổi. Mỗi dòng được đánh số bên dưới tương ứng với một chú thích số dòng trong mã ở trên:
- Mỗi tiện ích đều xác định một phương thức
build()
được tự động gọi mỗi khi hoàn cảnh của tiện ích thay đổi để tiện ích luôn được cập nhật. MyHomePage
theo dõi các thay đổi đối với trạng thái hiện tại của ứng dụng bằng phương thứcwatch
.- Mọi phương thức
build
đều phải trả về một tiện ích hoặc (thường xuyên hơn) một cây tiện ích lồng nhau. Trong trường hợp này, tiện ích cấp cao nhất làScaffold
. Bạn sẽ không làm việc vớiScaffold
trong lớp học lập trình này, nhưng đây là một tiện ích hữu ích và có trong phần lớn các ứng dụng Flutter thực tế. Column
là một trong những tiện ích bố cục cơ bản nhất trong Flutter. Thành phần này lấy một số lượng bất kỳ các thành phần con và đặt chúng vào một cột từ trên xuống dưới. Theo mặc định, cột sẽ đặt các phần tử con ở trên cùng. Bạn sẽ sớm thay đổi điều này để cột được căn giữa.- Bạn đã thay đổi tiện ích
Text
này ở bước đầu tiên. - Tiện ích
Text
thứ hai này lấyappState
và truy cập vào thành viên duy nhất của lớp đó,current
(là mộtWordPair
).WordPair
cung cấp một số phương thức truy xuất hữu ích, chẳng hạn nhưasPascalCase
hoặcasSnakeCase
. Ở đây, chúng ta dùngasLowerCase
nhưng bạn có thể thay đổi giá trị này ngay bây giờ nếu muốn dùng một trong các lựa chọn thay thế. - Lưu ý cách mã Flutter sử dụng nhiều dấu phẩy ở cuối. Bạn không cần dùng dấu phẩy này vì
children
là thành phần cuối cùng (và cũng là thành phần duy nhất) của danh sách tham sốColumn
này. Tuy nhiên, bạn nên sử dụng dấu phẩy ở cuối: dấu phẩy này giúp việc thêm nhiều thành viên trở nên dễ dàng và cũng đóng vai trò là gợi ý để trình định dạng tự động của Dart đặt một dòng mới ở đó. Để biết thêm thông tin, hãy xem phần Định dạng mã.
Tiếp theo, bạn sẽ kết nối nút với trạng thái.
Hành vi đầu tiên của bạn
Di chuyển đến MyAppState
rồi thêm phương thức getNext
.
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
// ↓ Add this.
void getNext() {
current = WordPair.random();
notifyListeners();
}
}
// ...
Phương thức getNext()
mới sẽ chỉ định lại current
bằng một WordPair
ngẫu nhiên mới. Phương thức này cũng gọi notifyListeners()
(một phương thức của ChangeNotifier)
đảm bảo rằng mọi người xem MyAppState
đều nhận được thông báo.
Tất cả những gì còn lại là gọi phương thức getNext
từ lệnh gọi lại của nút.
lib/main.dart
// ...
ElevatedButton(
onPressed: () {
appState.getNext(); // ← This instead of print().
},
child: Text('Next'),
),
// ...
Lưu và dùng thử ứng dụng ngay. Ứng dụng sẽ tạo một cặp từ ngẫu nhiên mới mỗi khi bạn nhấn nút Tiếp theo.
Trong phần tiếp theo, bạn sẽ làm cho giao diện người dùng trở nên đẹp mắt hơn.
5. Làm cho ứng dụng đẹp hơn
Đây là giao diện của ứng dụng hiện tại.
Không tốt. Phần trung tâm của ứng dụng (cặp từ được tạo ngẫu nhiên) sẽ dễ thấy hơn. Suy cho cùng, đây là lý do chính khiến người dùng sử dụng ứng dụng này! Ngoài ra, nội dung ứng dụng bị lệch tâm một cách kỳ lạ và toàn bộ ứng dụng chỉ có màu đen và trắng.
Phần này giải quyết những vấn đề này bằng cách cải thiện thiết kế của ứng dụng. Mục tiêu cuối cùng của phần này là một nội dung như sau:
Trích xuất tiện ích
Dòng chịu trách nhiệm hiển thị cặp từ hiện tại hiện có dạng như sau: Text(appState.current.asLowerCase)
. Để thay đổi thành một thứ gì đó phức tạp hơn, bạn nên trích xuất dòng này thành một tiện ích riêng biệt. Việc có các tiện ích riêng biệt cho các phần logic riêng biệt của giao diện người dùng là một cách quan trọng để quản lý độ phức tạp trong Flutter.
Flutter cung cấp một trình trợ giúp tái cấu trúc để trích xuất các tiện ích, nhưng trước khi sử dụng, hãy đảm bảo rằng dòng được trích xuất chỉ truy cập vào những gì cần thiết. Hiện tại, dòng này truy cập vào appState
, nhưng thực sự chỉ cần biết cặp từ hiện tại là gì.
Vì lý do đó, hãy viết lại tiện ích MyHomePage
như sau:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current; // ← Add this.
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(pair.asLowerCase), // ← Change to this.
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
Tuyệt. Tiện ích Text
không còn đề cập đến toàn bộ appState
nữa.
Bây giờ, hãy gọi trình đơn Refactor (Tái cấu trúc). Trong VS Code, bạn có thể thực hiện việc này theo một trong hai cách:
- Nhấp chuột phải vào đoạn mã mà bạn muốn tái cấu trúc (
Text
trong trường hợp này) rồi chọn Refactor... (Tái cấu trúc...) trong trình đơn thả xuống,
HOẶC
- Di chuyển con trỏ đến đoạn mã mà bạn muốn tái cấu trúc (
Text
trong trường hợp này) rồi nhấnCtrl+.
(Win/Linux) hoặcCmd+.
(Mac).
Trong trình đơn Refactor (Tái cấu trúc), hãy chọn Extract Widget (Trích xuất tiện ích). Chỉ định một tên, chẳng hạn như BigCard, rồi nhấp vào Enter
.
Thao tác này sẽ tự động tạo một lớp mới, BigCard
, ở cuối tệp hiện tại. Lớp này có dạng như sau:
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Text(pair.asLowerCase);
}
}
// ...
Lưu ý cách ứng dụng tiếp tục hoạt động ngay cả trong quá trình tái cấu trúc này.
Thêm thẻ
Giờ là lúc biến tiện ích mới này thành phần giao diện người dùng nổi bật mà chúng ta đã hình dung ở đầu phần này.
Tìm lớp BigCard
và phương thức build()
trong lớp đó. Như trước đây, hãy gọi trình đơn Refactor (Tái cấu trúc) trên tiện ích Text
. Tuy nhiên, lần này bạn sẽ không trích xuất tiện ích.
Thay vào đó, hãy chọn Bọc bằng khoảng đệm. Thao tác này sẽ tạo một tiện ích mẹ mới xung quanh tiện ích Text
có tên là Padding
. Sau khi lưu, bạn sẽ thấy từ ngẫu nhiên đó đã có nhiều khoảng trống hơn.
Tăng khoảng đệm từ giá trị mặc định là 8.0
. Ví dụ: sử dụng một giá trị như 20
để có khoảng đệm rộng rãi hơn.
Tiếp theo, hãy tăng thêm một cấp. Đặt con trỏ lên tiện ích Padding
, kéo trình đơn Refactor (Tái cấu trúc) lên rồi chọn Wrap with widget... (Bọc bằng tiện ích...).
Điều này cho phép bạn chỉ định tiện ích mẹ. Nhập "Thẻ" rồi nhấn Enter.
Thao tác này sẽ bao bọc tiện ích Padding
và do đó cũng bao bọc Text
bằng tiện ích Card
.
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
}
// ...
Giờ đây, ứng dụng sẽ có dạng như sau:
Giao diện và kiểu
Để thẻ nổi bật hơn, hãy tô màu đậm hơn cho thẻ. Vì luôn nên duy trì bảng phối màu nhất quán, hãy dùng Theme
của ứng dụng để chọn màu.
Thực hiện các thay đổi sau đối với phương thức build()
của BigCard
.
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // ← Add this.
return Card(
color: theme.colorScheme.primary, // ← And also this.
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
// ...
Hai dòng mới này thực hiện rất nhiều việc:
- Trước tiên, mã sẽ yêu cầu giao diện hiện tại của ứng dụng bằng
Theme.of(context)
. - Sau đó, mã này xác định màu của thẻ giống với thuộc tính
colorScheme
của giao diện. Bảng phối màu chứa nhiều màu vàprimary
là màu nổi bật nhất, xác định màu của ứng dụng.
Thẻ hiện được vẽ bằng màu chính của ứng dụng:
Bạn có thể thay đổi màu này và bảng phối màu của toàn bộ ứng dụng bằng cách di chuyển lên MyApp
rồi thay đổi màu gốc cho ColorScheme
tại đó.
Hãy lưu ý cách màu sắc chuyển động mượt mà. Đây được gọi là ảnh động ngầm. Nhiều tiện ích Flutter sẽ nội suy mượt mà giữa các giá trị để giao diện người dùng không chỉ "nhảy" giữa các trạng thái.
Nút có độ nâng bên dưới thẻ cũng thay đổi màu. Đó là sức mạnh của việc sử dụng Theme
trên toàn ứng dụng thay vì cố định giá trị trong mã.
TextTheme
Thẻ vẫn có vấn đề: văn bản quá nhỏ và màu sắc khó đọc. Để khắc phục vấn đề này, hãy thực hiện các thay đổi sau đối với phương thức build()
của BigCard
.
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// ↓ Add this.
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Change this line.
child: Text(pair.asLowerCase, style: style),
),
);
}
// ...
Nguyên nhân của thay đổi này:
- Bằng cách sử dụng
theme.textTheme,
, bạn sẽ truy cập vào giao diện phông chữ của ứng dụng. Lớp này bao gồm các thành phần nhưbodyMedium
(đối với văn bản tiêu chuẩn có kích thước trung bình),caption
(đối với chú thích của hình ảnh) hoặcheadlineLarge
(đối với tiêu đề lớn). - Thuộc tính
displayMedium
là một kiểu lớn dùng để hiển thị văn bản. Từ display (hiển thị) được dùng theo nghĩa in ấn ở đây, chẳng hạn như trong kiểu chữ hiển thị. Tài liệu chodisplayMedium
cho biết rằng "kiểu hiển thị được dành riêng cho văn bản ngắn và quan trọng" – đúng với trường hợp sử dụng của chúng ta. - Theo lý thuyết, thuộc tính
displayMedium
của giao diện có thể lànull
. Dart (ngôn ngữ lập trình mà bạn đang dùng để viết ứng dụng này) là ngôn ngữ an toàn rỗng, nên ngôn ngữ này sẽ không cho phép bạn gọi các phương thức của những đối tượng có khả năng lànull
. Tuy nhiên, trong trường hợp này, bạn có thể sử dụng toán tử!
("toán tử dấu chấm than") để đảm bảo Dart biết bạn đang làm gì. (displayMedium
chắc chắn không có giá trị rỗng trong trường hợp này. Lý do chúng ta biết điều này nằm ngoài phạm vi của lớp học lập trình này.) - Việc gọi
copyWith()
trêndisplayMedium
sẽ trả về một bản sao của kiểu văn bản với những thay đổi mà bạn xác định. Trong trường hợp này, bạn chỉ thay đổi màu của văn bản. - Để lấy màu mới, bạn sẽ truy cập lại vào giao diện của ứng dụng. Thuộc tính
onPrimary
của bảng phối màu xác định một màu phù hợp để sử dụng trên màu chính của ứng dụng.
Bây giờ, ứng dụng sẽ có dạng như sau:
Nếu muốn, bạn có thể thay đổi thẻ thêm nữa. Dưới đây là một số ý tưởng:
copyWith()
cho phép bạn thay đổi nhiều yếu tố khác về kiểu chữ chứ không chỉ màu sắc. Để xem danh sách đầy đủ các thuộc tính mà bạn có thể thay đổi, hãy đặt con trỏ vào bất kỳ vị trí nào bên trong dấu ngoặc đơn củacopyWith()
rồi nhấnCtrl+Shift+Space
(Win/Linux) hoặcCmd+Shift+Space
(Mac).- Tương tự, bạn có thể thay đổi thêm về tiện ích
Card
. Ví dụ: bạn có thể phóng to bóng của thẻ bằng cách tăng giá trị của tham sốelevation
. - Hãy thử nghiệm với các màu sắc. Ngoài
theme.colorScheme.primary
, còn có.secondary
,.surface
và vô số các loại khác. Tất cả các màu này đều có màu tương đươngonPrimary
.
Cải thiện khả năng hỗ trợ tiếp cận
Theo mặc định, Flutter giúp ứng dụng dễ tiếp cận. Ví dụ: mọi ứng dụng Flutter đều hiển thị chính xác tất cả văn bản và các phần tử tương tác trong ứng dụng cho trình đọc màn hình như TalkBack và VoiceOver.
Tuy nhiên, đôi khi bạn cần phải làm một số việc. Trong trường hợp ứng dụng này, trình đọc màn hình có thể gặp vấn đề khi phát âm một số cặp từ được tạo. Mặc dù con người không gặp vấn đề khi xác định hai từ trong cheaphead, nhưng trình đọc màn hình có thể phát âm ph ở giữa từ là f.
Giải pháp là thay thế pair.asLowerCase
bằng "${pair.first} ${pair.second}"
. Phương thức thứ hai sử dụng tính năng nội suy chuỗi để tạo một chuỗi (chẳng hạn như "cheap head"
) từ hai từ có trong pair
. Việc sử dụng hai từ riêng biệt thay vì một từ ghép sẽ giúp trình đọc màn hình xác định chúng một cách thích hợp và mang lại trải nghiệm tốt hơn cho người dùng khiếm thị.
Tuy nhiên, bạn nên giữ cho pair.asLowerCase
có hình ảnh đơn giản. Sử dụng thuộc tính Text
của semanticsLabel
để ghi đè nội dung trực quan của tiện ích văn bản bằng nội dung ngữ nghĩa phù hợp hơn cho trình đọc màn hình:
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Make the following change.
child: Text(
pair.asLowerCase,
style: style,
semanticsLabel: "${pair.first} ${pair.second}",
),
),
);
}
// ...
Giờ đây, trình đọc màn hình phát âm chính xác từng cặp từ được tạo, nhưng giao diện người dùng vẫn giữ nguyên. Hãy thử tính năng này bằng cách sử dụng trình đọc màn hình trên thiết bị.
Căn giữa giao diện người dùng
Giờ đây, khi cặp từ ngẫu nhiên đã được trình bày với đủ nét đặc sắc về hình ảnh, đã đến lúc đặt cặp từ đó vào giữa cửa sổ/màn hình của ứng dụng.
Trước tiên, hãy nhớ rằng BigCard
là một phần của Column
. Theo mặc định, các cột sẽ dồn các thành phần con lên trên cùng, nhưng chúng ta có thể ghi đè hành vi này. Chuyển đến phương thức build()
của MyHomePage
rồi thực hiện thay đổi sau:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center, // ← Add this.
children: [
Text('A random AWESOME idea:'),
BigCard(pair: pair),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
Thao tác này sẽ căn giữa các phần tử con bên trong Column
dọc theo trục chính (trục dọc).
Các phần tử con đã được căn giữa dọc theo trục chéo của cột (nói cách khác, các phần tử con đã được căn giữa theo chiều ngang). Nhưng Column
chính nó không được căn giữa bên trong Scaffold
. Chúng ta có thể xác minh điều này bằng cách sử dụng Widget Inspector (Công cụ kiểm tra tiện ích).
Bản thân Widget Inspector nằm ngoài phạm vi của lớp học lập trình này, nhưng bạn có thể thấy rằng khi Column
được làm nổi bật, nó không chiếm toàn bộ chiều rộng của ứng dụng. Nó chỉ chiếm không gian theo chiều ngang khi các thành phần con cần.
Bạn chỉ cần căn giữa cột. Đặt con trỏ lên Column
, gọi trình đơn Refactor (Tái cấu trúc) (bằng Ctrl+.
hoặc Cmd+.
) rồi chọn Wrap with Center (Bọc bằng Center).
Bây giờ, ứng dụng sẽ có dạng như sau:
Nếu muốn, bạn có thể điều chỉnh thêm.
- Bạn có thể xoá tiện ích
Text
ở phía trênBigCard
. Có thể cho rằng văn bản mô tả ("Một ý tưởng TUYỆT VỜI ngẫu nhiên:") không còn cần thiết nữa vì giao diện người dùng vẫn có ý nghĩa ngay cả khi không có văn bản này. Và cách này sẽ gọn gàng hơn. - Bạn cũng có thể thêm tiện ích
SizedBox(height: 10)
trong khoảng thời gian từBigCard
đếnElevatedButton
. Bằng cách này, sẽ có một khoảng cách lớn hơn một chút giữa hai tiện ích. Tiện íchSizedBox
chỉ chiếm dung lượng và không tự hiển thị nội dung nào. Thẻ này thường được dùng để tạo "khoảng trống" trực quan.
Với các thay đổi không bắt buộc, MyHomePage
sẽ chứa mã sau:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
),
);
}
}
// ...
Ứng dụng sẽ có giao diện như sau:
Trong phần tiếp theo, bạn sẽ thêm khả năng đánh dấu từ đã tạo là mục yêu thích (hoặc "thích").
6. Thêm chức năng
Ứng dụng hoạt động và đôi khi còn cung cấp các cặp từ thú vị. Nhưng mỗi khi người dùng nhấp vào Tiếp theo, mỗi cặp từ sẽ biến mất vĩnh viễn. Sẽ tốt hơn nếu có cách để "ghi nhớ" những đề xuất hay nhất, chẳng hạn như nút "Thích".
Thêm logic nghiệp vụ
Di chuyển đến MyAppState
rồi thêm đoạn mã sau:
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
void getNext() {
current = WordPair.random();
notifyListeners();
}
// ↓ Add the code below.
var favorites = <WordPair>[];
void toggleFavorite() {
if (favorites.contains(current)) {
favorites.remove(current);
} else {
favorites.add(current);
}
notifyListeners();
}
}
// ...
Kiểm tra các thay đổi:
- Bạn đã thêm một thuộc tính mới vào
MyAppState
có tên làfavorites
. Thuộc tính này được khởi tạo bằng một danh sách trống:[]
. - Bạn cũng chỉ định rằng danh sách chỉ có thể chứa các cặp từ:
<WordPair>[]
, bằng cách sử dụng các kiểu chung. Điều này giúp ứng dụng của bạn trở nên mạnh mẽ hơn – Dart thậm chí sẽ từ chối chạy ứng dụng nếu bạn cố gắng thêm bất kỳ thứ gì khác ngoàiWordPair
vào ứng dụng. Đổi lại, bạn có thể dùng danh sáchfavorites
mà không cần lo lắng vì không bao giờ có bất kỳ đối tượng không mong muốn nào (chẳng hạn nhưnull
) ẩn trong đó.
- Bạn cũng đã thêm một phương thức mới,
toggleFavorite()
, phương thức này sẽ xoá cặp từ hiện tại khỏi danh sách yêu thích (nếu cặp từ đó đã có trong danh sách) hoặc thêm cặp từ đó (nếu cặp từ đó chưa có trong danh sách). Trong cả hai trường hợp, mã sẽ gọinotifyListeners();
sau đó.
Thêm nút
Sau khi hoàn tất "logic nghiệp vụ", bạn có thể bắt đầu xử lý giao diện người dùng. Để đặt nút "Thích" ở bên trái nút "Tiếp theo", bạn cần có Row
. Tiện ích Row
là phiên bản ngang của Column
mà bạn đã thấy trước đó.
Trước tiên, hãy gói nút hiện có trong một Row
. Chuyển đến phương thức MyHomePage
, đặt con trỏ lên ElevatedButton
, gọi trình đơn Refactor (Tái cấu trúc) bằng Ctrl+.
hoặc Cmd+.
, rồi chọn Wrap with Row (Bọc bằng hàng).build()
Khi lưu, bạn sẽ nhận thấy rằng Row
hoạt động tương tự như Column
– theo mặc định, nó sẽ gộp các thành phần con ở bên trái. (Column
gộp các phần tử con của nó lên trên cùng.) Để khắc phục vấn đề này, bạn có thể sử dụng phương pháp tương tự như trước đây, nhưng với mainAxisAlignment
. Tuy nhiên, đối với mục đích giáo huấn (học tập), hãy sử dụng mainAxisSize
. Điều này cho biết Row
không được chiếm hết không gian ngang có sẵn.
Thực hiện thay đổi sau:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min, // ← Add this.
children: [
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
Giao diện người dùng sẽ trở lại trạng thái trước đó.
Tiếp theo, hãy thêm nút Thích và kết nối nút này với toggleFavorite()
. Để thử thách bản thân, trước tiên, hãy cố gắng tự làm việc này mà không cần xem khối mã bên dưới.
Bạn không cần phải làm theo chính xác cách làm dưới đây. Thực tế là bạn không cần lo lắng về biểu tượng trái tim trừ phi bạn thực sự muốn một thử thách lớn.
Bạn cũng hoàn toàn có thể thất bại – dù sao thì đây cũng là giờ đầu tiên bạn làm việc với Flutter.
Sau đây là một cách để thêm nút thứ hai vào MyHomePage
. Lần này, hãy dùng hàm khởi tạo ElevatedButton.icon()
để tạo một nút có biểu tượng. Và ở đầu phương thức build
, hãy chọn biểu tượng thích hợp tuỳ thuộc vào việc cặp từ hiện tại đã có trong mục yêu thích hay chưa. Ngoài ra, hãy lưu ý việc sử dụng lại SizedBox
để giữ cho hai nút cách nhau một chút.
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
// ↓ Add this.
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// ↓ And this.
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
Ứng dụng sẽ có dạng như sau:
Rất tiếc, người dùng không thể xem các mục yêu thích. Đã đến lúc thêm một màn hình hoàn toàn riêng biệt vào ứng dụng của chúng ta. Hẹn gặp lại bạn ở phần tiếp theo!
7. Thêm dải điều hướng
Hầu hết các ứng dụng đều không thể đưa mọi thứ vào một màn hình duy nhất. Ứng dụng cụ thể này có thể làm được, nhưng vì mục đích giảng dạy, bạn sẽ tạo một màn hình riêng cho mục yêu thích của người dùng. Để chuyển đổi giữa hai màn hình, bạn sẽ triển khai StatefulWidget
đầu tiên.
Để đi vào trọng tâm của bước này càng sớm càng tốt, hãy chia MyHomePage
thành 2 tiện ích riêng biệt.
Chọn tất cả MyHomePage
, xoá rồi thay thế bằng mã sau:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: 0,
onDestinationSelected: (value) {
print('selected: $value');
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
class GeneratorPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
);
}
}
// ...
Khi lưu, bạn sẽ thấy mặt trực quan của giao diện người dùng đã sẵn sàng nhưng không hoạt động. Khi bạn nhấp vào biểu tượng ♥︎ (trái tim) trong thanh điều hướng, sẽ không có gì xảy ra.
Kiểm tra các thay đổi.
- Trước tiên, hãy lưu ý rằng toàn bộ nội dung của
MyHomePage
được trích xuất vào một tiện ích mới,GeneratorPage
. Phần duy nhất của tiện íchMyHomePage
cũ không được trích xuất làScaffold
. MyHomePage
mới chứa mộtRow
có hai phần tử con. Tiện ích đầu tiên làSafeArea
, còn tiện ích thứ hai làExpanded
.SafeArea
đảm bảo rằng thành phần con của nó không bị che khuất bởi một vết cắt trên phần cứng hoặc thanh trạng thái. Trong ứng dụng này, tiện ích sẽ bao quanhNavigationRail
để ngăn các nút điều hướng bị thanh trạng thái trên thiết bị di động che khuất, chẳng hạn như vậy.- Bạn có thể thay đổi dòng
extended: false
trongNavigationRail
thànhtrue
. Thao tác này sẽ hiển thị nhãn bên cạnh biểu tượng. Trong một bước tiếp theo, bạn sẽ tìm hiểu cách thực hiện việc này một cách tự động khi ứng dụng có đủ không gian theo chiều ngang. - Thanh điều hướng có 2 đích đến (Trang chủ và Mục yêu thích), cùng các biểu tượng và nhãn tương ứng. Phần này cũng xác định
selectedIndex
hiện tại. Chỉ mục đã chọn là 0 sẽ chọn đích đến đầu tiên, chỉ mục đã chọn là 1 sẽ chọn đích đến thứ hai, v.v. Hiện tại, giá trị này được mã hoá cứng thành 0. - Thanh điều hướng cũng xác định những gì xảy ra khi người dùng chọn một trong các đích đến bằng
onDestinationSelected
. Hiện tại, ứng dụng chỉ xuất giá trị chỉ mục được yêu cầu bằngprint()
. - Phần tử con thứ hai của
Row
là tiện íchExpanded
. Các tiện ích mở rộng rất hữu ích trong các hàng và cột. Chúng cho phép bạn thể hiện bố cục trong đó một số thành phần con chỉ chiếm không gian cần thiết (SafeArea
trong trường hợp này) và các tiện ích khác sẽ chiếm nhiều không gian còn lại nhất có thể (Expanded
trong trường hợp này). Một cách để liên tưởng đến các tiện íchExpanded
là chúng "tham lam". Nếu bạn muốn hiểu rõ hơn về vai trò của tiện ích này, hãy thử bao bọc tiện íchSafeArea
bằng mộtExpanded
khác. Bố cục kết quả sẽ có dạng như sau:
- Hai tiện ích
Expanded
chia đều tất cả không gian theo chiều ngang có sẵn giữa chúng, mặc dù thanh điều hướng chỉ thực sự cần một phần nhỏ ở bên trái. - Bên trong tiện ích
Expanded
, có mộtContainer
có màu và bên trong vùng chứa làGeneratorPage
.
Tiện ích không trạng thái so với tiện ích có trạng thái
Cho đến nay, MyAppState
đã đáp ứng mọi nhu cầu về trạng thái của bạn. Đó là lý do tất cả các tiện ích bạn đã viết cho đến nay đều là tiện ích không trạng thái. Chúng không chứa bất kỳ trạng thái có thể thay đổi nào của riêng chúng. Không có tiện ích nào có thể tự thay đổi chính nó. Các tiện ích phải thông qua MyAppState
.
Điều này sắp thay đổi.
Bạn cần một cách nào đó để giữ giá trị của selectedIndex
trong thanh điều hướng. Bạn cũng muốn có thể thay đổi giá trị này trong lệnh gọi lại onDestinationSelected
.
Bạn có thể thêm selectedIndex
làm một thuộc tính khác của MyAppState
. Và cách này sẽ hiệu quả. Nhưng bạn có thể hình dung rằng trạng thái ứng dụng sẽ nhanh chóng tăng lên một cách vô lý nếu mọi tiện ích đều lưu trữ các giá trị của nó trong trạng thái ứng dụng.
Một số trạng thái chỉ liên quan đến một tiện ích duy nhất, vì vậy, trạng thái đó phải đi kèm với tiện ích đó.
Nhập StatefulWidget
, một loại tiện ích có State
. Trước tiên, hãy chuyển đổi MyHomePage
thành một tiện ích có trạng thái.
Đặt con trỏ lên dòng đầu tiên của MyHomePage
(dòng bắt đầu bằng class MyHomePage...
) rồi gọi trình đơn Refactor (Tái cấu trúc) bằng cách sử dụng Ctrl+.
hoặc Cmd+.
. Sau đó, hãy chọn Convert to StatefulWidget (Chuyển đổi thành StatefulWidget).
IDE sẽ tạo một lớp mới cho bạn, _MyHomePageState
. Lớp này mở rộng State
và do đó có thể quản lý các giá trị của riêng lớp. (Nó có thể tự thay đổi.) Ngoài ra, xin lưu ý rằng phương thức build
từ tiện ích cũ, không trạng thái đã chuyển sang _MyHomePageState
(thay vì vẫn nằm trong tiện ích). Nội dung này được di chuyển nguyên văn – không có gì thay đổi trong phương thức build
. Giờ đây, nó chỉ đơn giản là nằm ở một nơi khác.
setState
Tiện ích có trạng thái mới chỉ cần theo dõi một biến: selectedIndex
. Thực hiện 3 thay đổi sau đối với _MyHomePageState
:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0; // ← Add this property.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex, // ← Change to this.
onDestinationSelected: (value) {
// ↓ Replace print with this.
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
// ...
Kiểm tra các thay đổi:
- Bạn giới thiệu một biến mới,
selectedIndex
, và khởi tạo biến đó thành0
. - Bạn sử dụng biến mới này trong định nghĩa
NavigationRail
thay vì0
được mã hoá cứng cho đến nay. - Khi lệnh gọi lại
onDestinationSelected
được gọi, thay vì chỉ in giá trị mới vào bảng điều khiển, bạn sẽ chỉ định giá trị đó choselectedIndex
bên trong lệnh gọisetState()
. Lệnh gọi này tương tự như phương thứcnotifyListeners()
được dùng trước đây – phương thức này đảm bảo rằng giao diện người dùng sẽ cập nhật.
Thanh điều hướng hiện phản hồi hoạt động tương tác của người dùng. Nhưng vùng mở rộng ở bên phải vẫn giữ nguyên. Đó là vì mã này không dùng selectedIndex
để xác định màn hình nào sẽ hiển thị.
Sử dụng selectedIndex
Đặt đoạn mã sau ở đầu phương thức build
của _MyHomePageState
, ngay trước return Scaffold
:
lib/main.dart
// ...
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
// ...
Hãy xem xét đoạn mã này:
- Mã này khai báo một biến mới,
page
, thuộc loạiWidget
. - Sau đó, một câu lệnh switch sẽ chỉ định một màn hình cho
page
, theo giá trị hiện tại trongselectedIndex
. - Vì chưa có
FavoritesPage
, hãy dùngPlaceholder
; đây là một tiện ích hữu ích vẽ một hình chữ nhật có đường chéo ở bất cứ nơi nào bạn đặt, đánh dấu phần đó của giao diện người dùng là chưa hoàn thành.
- Khi áp dụng nguyên tắc thất bại nhanh, câu lệnh switch cũng đảm bảo sẽ đưa ra lỗi nếu
selectedIndex
không phải là 0 hoặc 1. Điều này giúp ngăn chặn các lỗi trong tương lai. Nếu bạn thêm một đích đến mới vào thanh điều hướng và quên cập nhật mã này, chương trình sẽ gặp sự cố trong quá trình phát triển (thay vì để bạn đoán lý do khiến mọi thứ không hoạt động hoặc cho phép bạn xuất bản mã có lỗi vào bản phát hành công khai).
Giờ đây, page
chứa tiện ích mà bạn muốn hiển thị ở bên phải, có lẽ bạn có thể đoán được những thay đổi khác cần thiết.
Sau đây là _MyHomePageState
sau thay đổi duy nhất còn lại:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page, // ← Here.
),
),
],
),
);
}
}
// ...
Giờ đây, ứng dụng sẽ chuyển đổi giữa GeneratorPage
và phần giữ chỗ sắp trở thành trang Mục yêu thích.
Phản ứng nhanh
Tiếp theo, hãy điều chỉnh dải điều hướng cho phù hợp. Tức là tự động hiện các nhãn (bằng cách sử dụng extended: true
) khi có đủ không gian cho các nhãn đó.
Flutter cung cấp một số tiện ích giúp bạn tạo ứng dụng tự động thích ứng. Ví dụ: Wrap
là một tiện ích tương tự như Row
hoặc Column
, tự động bao bọc các phần tử con vào "dòng" tiếp theo (gọi là "chạy") khi không có đủ không gian theo chiều dọc hoặc chiều ngang. Có FittedBox
, một tiện ích tự động điều chỉnh phần tử con cho vừa không gian hiện có theo quy cách của bạn.
Tuy nhiên, NavigationRail
không tự động hiện nhãn khi có đủ không gian vì không thể biết đủ không gian trong mọi ngữ cảnh. Bạn (nhà phát triển) sẽ quyết định việc đó.
Giả sử bạn quyết định chỉ hiển thị nhãn nếu MyHomePage
có chiều rộng ít nhất là 600 pixel.
Tiện ích cần sử dụng trong trường hợp này là LayoutBuilder
. Bạn có thể thay đổi cây tiện ích tuỳ theo dung lượng trống.
Một lần nữa, hãy dùng trình đơn Refactor (Tái cấu trúc) của Flutter trong VS Code để thực hiện các thay đổi cần thiết. Tuy nhiên, lần này, vấn đề phức tạp hơn một chút:
- Bên trong phương thức
build
của_MyHomePageState
, hãy đặt con trỏ lênScaffold
. - Gọi trình đơn Refactor (Tái cấu trúc) bằng
Ctrl+.
(Windows/Linux) hoặcCmd+.
(Mac). - Chọn Wrap with Builder (Bọc bằng Trình tạo) rồi nhấn phím Enter.
- Sửa đổi tên của
Builder
mới thêm thànhLayoutBuilder
. - Sửa đổi danh sách tham số gọi lại từ
(context)
thành(context, constraints)
.
Lệnh gọi lại builder
của LayoutBuilder
được gọi mỗi khi các điều kiện ràng buộc thay đổi. Điều này xảy ra khi, chẳng hạn như:
- Người dùng đổi kích thước cửa sổ của ứng dụng
- Người dùng xoay điện thoại từ chế độ dọc sang chế độ ngang hoặc ngược lại
- Một số tiện ích bên cạnh
MyHomePage
tăng kích thước, khiến các quy tắc ràng buộc củaMyHomePage
nhỏ hơn
Giờ đây, mã của bạn có thể quyết định xem có hiển thị nhãn hay không bằng cách truy vấn constraints
hiện tại. Thực hiện thay đổi sau đây trên một dòng cho phương thức build
của _MyHomePageState
:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return LayoutBuilder(builder: (context, constraints) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: constraints.maxWidth >= 600, // ← Here.
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page,
),
),
],
),
);
});
}
}
// ...
Giờ đây, ứng dụng của bạn sẽ phản hồi theo môi trường, chẳng hạn như kích thước màn hình, hướng và nền tảng! Nói cách khác, nó có khả năng thích ứng!
Việc duy nhất còn lại là thay thế Placeholder
đó bằng màn hình Mục yêu thích thực tế. Nội dung này sẽ được đề cập trong phần tiếp theo.
8. Thêm trang mới
Bạn có nhớ tiện ích Placeholder
mà chúng ta đã dùng thay cho trang Mục yêu thích không?
Đã đến lúc khắc phục vấn đề này.
Nếu bạn muốn thử thách bản thân, hãy tự mình thực hiện bước này. Mục tiêu của bạn là hiển thị danh sách favorites
trong một tiện ích không trạng thái mới, FavoritesPage
, rồi hiển thị tiện ích đó thay vì Placeholder
.
Sau đây là một vài gợi ý:
- Khi bạn muốn một
Column
có thể cuộn, hãy sử dụng tiện íchListView
. - Hãy nhớ rằng bạn có thể truy cập vào thực thể
MyAppState
từ mọi tiện ích bằng cách sử dụngcontext.watch<MyAppState>()
. - Nếu bạn cũng muốn thử một tiện ích mới,
ListTile
có các thuộc tính nhưtitle
(thường dùng cho văn bản),leading
(dùng cho biểu tượng hoặc hình đại diện) vàonTap
(dùng cho các lượt tương tác). Tuy nhiên, bạn có thể đạt được hiệu ứng tương tự bằng các tiện ích mà bạn đã biết. - Dart cho phép sử dụng các vòng lặp
for
bên trong các giá trị cố định của bộ sưu tập. Ví dụ: nếumessages
chứa một danh sách các chuỗi, bạn có thể có mã như sau:
Mặt khác, nếu bạn quen thuộc hơn với lập trình hàm, thì Dart cũng cho phép bạn viết mã như messages.map((m) => Text(m)).toList()
. Và tất nhiên, bạn luôn có thể tạo một danh sách các tiện ích và thêm tiện ích vào danh sách đó một cách bắt buộc trong phương thức build
.
Lợi ích của việc tự thêm trang Yêu thích là bạn sẽ học được nhiều hơn bằng cách tự đưa ra quyết định. Nhược điểm là bạn có thể gặp phải vấn đề mà bạn chưa thể tự giải quyết. Hãy nhớ rằng: thất bại không sao cả và là một trong những yếu tố quan trọng nhất của việc học. Không ai mong đợi bạn có thể phát triển ứng dụng bằng Flutter ngay trong giờ đầu tiên, và bạn cũng không nên mong đợi điều đó.
Sau đây chỉ là một cách triển khai trang yêu thích. Cách triển khai này (hy vọng) sẽ truyền cảm hứng để bạn thử nghiệm với mã, cải thiện giao diện người dùng và tạo ra giao diện người dùng của riêng mình.
Sau đây là lớp FavoritesPage
mới:
lib/main.dart
// ...
class FavoritesPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
if (appState.favorites.isEmpty) {
return Center(
child: Text('No favorites yet.'),
);
}
return ListView(
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text('You have '
'${appState.favorites.length} favorites:'),
),
for (var pair in appState.favorites)
ListTile(
leading: Icon(Icons.favorite),
title: Text(pair.asLowerCase),
),
],
);
}
}
Tiện ích này có những chức năng sau:
- Thao tác này sẽ lấy trạng thái hiện tại của ứng dụng.
- Nếu danh sách yêu thích trống, thì danh sách này sẽ hiển thị một thông báo ở giữa: Chưa có mục yêu thích nào.
- Nếu không, danh sách (có thể cuộn) sẽ xuất hiện.
- Danh sách bắt đầu bằng một bản tóm tắt (ví dụ: Bạn có 5 địa điểm yêu thích.).
- Sau đó, mã sẽ lặp lại tất cả các mục yêu thích và tạo một tiện ích
ListTile
cho từng mục.
Giờ đây, bạn chỉ cần thay thế tiện ích Placeholder
bằng FavoritesPage
. Và thế là xong!
Bạn có thể lấy đoạn mã cuối cùng của ứng dụng này trong kho lưu trữ lớp học lập trình trên GitHub.
9. Các bước tiếp theo
Xin chúc mừng!
Bạn làm tốt lắm! Bạn đã lấy một khung không hoạt động với một Column
và hai tiện ích Text
, rồi biến nó thành một ứng dụng nhỏ, thú vị và có khả năng thích ứng.
Nội dung đã đề cập
- Kiến thức cơ bản về cách hoạt động của Flutter
- Tạo bố cục trong Flutter
- Kết nối các lượt tương tác của người dùng (chẳng hạn như lượt nhấn nút) với hành vi của ứng dụng
- Sắp xếp mã Flutter gọn gàng
- Đảm bảo ứng dụng của bạn có khả năng phản hồi
- Đạt được giao diện nhất quán cho ứng dụng của bạn
Ðiều gì kế tiếp?
- Thử nghiệm thêm với ứng dụng mà bạn đã viết trong lớp học này.
- Hãy xem mã của phiên bản nâng cao này của cùng một ứng dụng để biết cách bạn có thể thêm danh sách động, chuyển màu, hiệu ứng mờ dần và nhiều hiệu ứng khác.
- Theo dõi lộ trình học tập của bạn bằng cách truy cập vào flutter.dev/learn.