MaterialStateProperty: Nút "Mood Ring" của Flutter!
Chào các "dev-genz" tương lai, lại là anh Creyt đây! Hôm nay, chúng ta sẽ "bóc phốt" một khái niệm mà thoạt nghe có vẻ hàn lâm nhưng thực ra lại cực kỳ "cool ngầu" và thiết thực trong Flutter: MaterialStateProperty.
1. "Mood Ring" của UI: MaterialStateProperty là gì và để làm gì?
Các em có bao giờ đeo cái nhẫn "mood ring" chưa? Nó đổi màu theo cảm xúc của mình ấy. Thì trong Flutter, các widget như nút bấm, checkbox, hay thậm chí cả text field cũng có "tâm trạng" riêng của chúng. Khi các em chạm vào, giữ, rê chuột qua, hoặc khi chúng bị vô hiệu hóa, chúng đều có một "trạng thái" (state) khác nhau.
MaterialStateProperty chính là "sổ tay hướng dẫn" để các widget của chúng ta "biến hình" theo những trạng thái đó! Thay vì phải viết "if-else" loằng ngoằng cho từng trạng thái màu sắc, kích thước, hay bóng đổ, MaterialStateProperty cho phép các em định nghĩa một cách mạch lạc rằng: "Khi ở trạng thái A thì trông như thế này, khi ở trạng thái B thì trông như thế khác".
Nói cách khác, nó là một lớp trừu tượng (abstract class) giúp chúng ta map một tập hợp các trạng thái (MaterialState) tới một giá trị cụ thể (ví dụ: một màu sắc, một kích thước, một hình dạng). Các trạng thái phổ biến mà chúng ta hay gặp là:
MaterialState.pressed: Khi widget bị nhấn.MaterialState.hovered: Khi con trỏ chuột rê qua (trên web/desktop).MaterialState.focused: Khi widget được focus (ví dụ, dùng tab để di chuyển).MaterialState.disabled: Khi widget bị vô hiệu hóa, không thể tương tác.MaterialState.selected: Khi widget được chọn (ví dụ, checkbox).
Nó "giải phóng" chúng ta khỏi việc phải tự quản lý từng trạng thái nhỏ nhặt, giúp code sạch hơn, dễ đọc hơn và "chuẩn Material Design" hơn.
2. Code Ví Dụ Minh Họa: "Thấy là hiểu ngay"
Để các em "thấm" ngay, chúng ta hãy xem một ví dụ kinh điển với ElevatedButton nhé. Anh Creyt sẽ "phù phép" cho cái nút này đổi màu nền khi được nhấn và khi bị vô hiệu hóa.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MaterialStateProperty Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _isButtonEnabled = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('MaterialStateProperty Explained'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _isButtonEnabled
? () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Nút đã được nhấn!')),
);
}
: null, // Khi null, nút sẽ tự động bị disabled
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return Colors.green.shade700; // Màu khi nhấn
}
if (states.contains(MaterialState.disabled)) {
return Colors.grey.shade400; // Màu khi bị vô hiệu hóa
}
return Theme.of(context).colorScheme.primary; // Màu mặc định
},
),
foregroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return Colors.white; // Chữ trắng khi nhấn
}
if (states.contains(MaterialState.disabled)) {
return Colors.grey.shade700; // Chữ xám đậm khi disabled
}
return Colors.white; // Chữ trắng mặc định
},
),
// Overlay color (hiệu ứng gợn sóng khi nhấn)
overlayColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return Colors.green.shade900.withOpacity(0.2);
}
if (states.contains(MaterialState.hovered)) {
return Colors.green.shade100.withOpacity(0.1);
}
return null; // Mặc định
},
),
// Shape (ví dụ: bo tròn hơn khi nhấn)
shape: MaterialStateProperty.resolveWith<OutlinedBorder?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
);
}
return const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8.0)),
);
},
),
// Elevation (độ nổi)
elevation: MaterialStateProperty.resolveWith<double?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return 10.0; // Nổi hơn khi nhấn
}
return 2.0; // Mặc định
},
),
),
child: const Text('Nhấn tôi đi!'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
setState(() {
_isButtonEnabled = !_isButtonEnabled;
});
},
child: Text(_isButtonEnabled ? 'Vô hiệu hóa nút trên' : 'Kích hoạt nút trên'),
),
],
),
),
);
}
}
Trong ví dụ trên:
- Chúng ta dùng
MaterialStateProperty.resolveWithđể cung cấp một hàm callback. Hàm này nhận vào mộtSet<MaterialState>(tập hợp các trạng thái hiện tại của widget) và trả về giá trị mong muốn (ví dụ:Color?). - Bên trong callback, chúng ta kiểm tra
states.contains(MaterialState.pressed)haystates.contains(MaterialState.disabled)để quyết định trả về màu gì. overlayColorlà màu hiển thị khi rê chuột hoặc nhấn, tạo hiệu ứng gợn sóng (splash effect).MaterialStateProperty.all<Color>(Colors.blue): Nếu các em muốn một giá trị không đổi dù widget ở bất kỳ trạng thái nào, thì dùngallcho gọn. Ví dụ, nếubackgroundColorluôn là xanh dương bất kể trạng thái nào.
3. Mẹo (Best Practices) từ "Lão Làng" Creyt:
- Đừng "lạm dụng" quá nhiều trạng thái: Mỗi lần đổi màu, đổi kích thước quá nhiều sẽ làm UI của các em trông "rối như canh hẹ". Chỉ nên thay đổi những thuộc tính quan trọng để người dùng dễ nhận biết trạng thái.
- Ưu tiên
ThemeData: Thay vì định nghĩaMaterialStatePropertycho từng nút một, hãy định nghĩa nó ở cấp độThemeData(ví dụ: trongElevatedButtonThemeData). Điều này giúp toàn bộ ứng dụng của các em có giao diện nhất quán và dễ bảo trì hơn rất nhiều. Coi như là "template" cho các nút vậy. - Sử dụng
MaterialStateProperty.allkhi không cần đổi: Nếu một thuộc tính nào đó (ví dụpadding) không cần thay đổi theo trạng thái, hãy dùngMaterialStateProperty.all(value)thay vìresolveWithđể code gọn gàng hơn. - Kiểm tra thứ tự trạng thái: Khi dùng
resolveWith, thứ tự cácifstatement quan trọng. Ví dụ,disabledthường có ưu tiên cao nhất, vì nó "ghi đè" lên các trạng thái khác. - Đảm bảo dễ tiếp cận (Accessibility): Luôn kiểm tra độ tương phản màu sắc giữa chữ và nền ở tất cả các trạng thái, đặc biệt là khi nút bị
disabledhoặcpressed, để người dùng có thị lực kém vẫn có thể nhận biết.
4. Ứng dụng Thực tế: "Ai cũng dùng, chỉ là không biết tên"
Các em có thể chưa biết tên MaterialStateProperty, nhưng chắc chắn đã dùng nó hàng ngày rồi:
- Google Apps (Gmail, Drive, Maps): Các nút bấm, checkbox, radio button đều có hiệu ứng khi nhấn, khi rê chuột (trên web), hoặc khi bị vô hiệu hóa. Đó chính là
MaterialStateProperty"đội lốt" ở phía dưới. - Facebook, Instagram: Khi các em nhấn nút "Like", nút "Comment", hay nút "Send" trong Messenger, chúng sẽ có một hiệu ứng nhấn, hoặc đổi màu để báo hiệu tương tác.
- Các ứng dụng E-commerce: Nút "Thêm vào giỏ hàng" thường bị mờ đi (disabled) khi sản phẩm hết hàng, và sáng lên khi có hàng. Khi nhấn, nó có thể đổi màu nhẹ để xác nhận thao tác.
Tóm lại, bất kỳ ứng dụng nào sử dụng Material Design và muốn có phản hồi trực quan mượt mà, chuyên nghiệp cho các tương tác của người dùng, đều sẽ dùng đến tư duy của MaterialStateProperty.
5. Thử nghiệm và Hướng dẫn nên dùng cho case nào?
Anh Creyt đã từng "ngây thơ" thử quản lý trạng thái của nút bằng cách tự tạo biến _isPressed = false; rồi setState khi onPressed và onLongPress. Kết quả là code rối rắm, khó mở rộng, và không bao giờ "chuẩn Material Design" được như cách Flutter sinh ra. Khi "khai sáng" ra MaterialStateProperty, mọi thứ trở nên "dễ thở" hơn rất nhiều.
Khi nào nên dùng?
- Tất cả các widget Material Design có thể tương tác:
ElevatedButton,TextButton,OutlinedButton,Checkbox,Radio,Switch,TabBar,TextField(ví dụ, màu border khi focused), v.v. - Khi các em muốn tuân thủ chặt chẽ Material Design: Nó là "công cụ vàng" để đạt được sự nhất quán và chuyên nghiệp.
- Khi muốn UI của mình "có hồn" hơn, phản hồi tốt hơn với người dùng.
Khi nào nên cân nhắc không dùng (hoặc dùng cách khác)?
- Với các widget hoàn toàn tùy chỉnh (custom widget) không dựa trên Material Design: Nếu các em tự vẽ mọi thứ từ đầu bằng
CustomPainthoặc các widget cấp thấp, có thể các em sẽ muốn quản lý trạng thái theo cách riêng của mình để có toàn quyền kiểm soát. - Khi chỉ cần một giá trị cố định, không thay đổi: Nếu một thuộc tính không bao giờ thay đổi theo trạng thái, thì chỉ cần truyền giá trị trực tiếp hoặc dùng
MaterialStateProperty.all.
Nhớ nhé, MaterialStateProperty không chỉ là một khái niệm, nó là một "triết lý" để xây dựng UI tương tác trong Flutter. Nắm vững nó, các em sẽ "nâng tầm" ứng dụng của mình lên một đẳng cấp mới! Chúc các em code vui vẻ và luôn "sáng tạo" nhé!
Thuộc Series: Flutter
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!