Loading...
Diamond Spa
Không có ngày
Flutter là một framework UI mạnh mẽ với khả năng tạo ra giao diện người dùng (UI) đẹp mắt và linh hoạt. Sức mạnh của Flutter nằm ở hệ thống widget phong phú, cho phép bạn tùy chỉnh gần như mọi khía cạnh của giao diện. Bài viết này sẽ giúp bạn hiểu rõ cách tận dụng các widget trong Flutter để xây dựng giao diện người dùng hấp dẫn.
Trong Flutter, mọi thứ đều là widget. Từ button đơn giản đến layout phức tạp, tất cả đều được xây dựng từ các widget. Hiểu một cách đơn giản, widget là các thành phần UI có thể tái sử dụng, giúp bạn xây dựng giao diện người dùng.
Flutter widget có thể được phân loại thành hai loại chính:
Text
, Icon
, Image
.Checkbox
, TextField
.Flutter sử dụng cấu trúc cây (tree) để tổ chức các widget. Mỗi widget có thể chứa các widget con, tạo thành một cây widget phức tạp. Hiểu về cấu trúc này rất quan trọng khi xây dựng giao diện Flutter.
// Ví dụ về Widget Tree
MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Ứng dụng Flutter'),
),
body: Center(
child: Column(
children: [
Text('Xin chào!'),
ElevatedButton(
onPressed: () {},
child: Text('Nhấn vào tôi'),
),
],
),
),
),
)
Hiểu vòng đời của widget sẽ giúp bạn quản lý giao diện hiệu quả hơn:
build
được gọi (nếu là StatelessWidget) hoặc createState
được gọi (nếu là StatefulWidget)setState
được gọi (đối với StatefulWidget)dispose
được gọi (đối với StatefulWidget)Widget Text
cho phép hiển thị văn bản với nhiều tùy chọn định dạng:
Text(
'Xin chào Flutter!',
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold,
color: Colors.blue,
letterSpacing: 1.5,
fontFamily: 'Roboto',
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
)
Các thuộc tính phổ biến của Text
:
style
: Định dạng văn bản (màu sắc, font, kích thước...)textAlign
: Căn chỉnh văn bản (left, center, right...)overflow
: Xử lý khi văn bản quá dàimaxLines
: Số dòng tối đa hiển thịFlutter cung cấp nhiều loại button khác nhau:
// Button phẳng với nền có màu
ElevatedButton(
onPressed: () {
print('Button được nhấn!');
},
style: ElevatedButton.styleFrom(
primary: Colors.blue,
onPrimary: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text('ElevatedButton'),
)
// Button với viền
OutlinedButton(
onPressed: () {},
style: OutlinedButton.styleFrom(
side: BorderSide(color: Colors.blue, width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text('OutlinedButton'),
)
// Button text đơn giản
TextButton(
onPressed: () {},
child: Text('TextButton'),
)
// Button với icon
IconButton(
icon: Icon(Icons.favorite),
color: Colors.red,
onPressed: () {},
)
Hiển thị hình ảnh từ nhiều nguồn khác nhau:
// Hình ảnh từ assets
Image.asset(
'assets/images/flutter_logo.png',
width: 200,
height: 200,
fit: BoxFit.cover,
)
// Hình ảnh từ internet
Image.network(
'https://flutter.dev/assets/flutter-lockup-1caf6476beed76adec3c477586da54de6b552b2f42108ec5bc68dc63bae2df75.png',
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
: null,
),
);
},
)
Các thuộc tính quan trọng:
width
, height
: Kích thước hình ảnhfit
: Cách hình ảnh được điều chỉnh để phù hợp với container (cover, contain, fill...)loadingBuilder
: Tùy chỉnh hiển thị trong khi hình ảnh đang tảiFlutter cung cấp một bộ icon phong phú:
Icon(
Icons.favorite,
color: Colors.red,
size: 30,
)
Ngoài ra, bạn có thể sử dụng các package bên ngoài để mở rộng bộ icon:
// Thêm vào pubspec.yaml:
// dependencies:
// font_awesome_flutter: ^10.1.0
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
FaIcon(
FontAwesomeIcons.github,
color: Colors.black,
size: 30,
)
Widget TextField
cho phép người dùng nhập liệu:
TextField(
decoration: InputDecoration(
labelText: 'Email',
hintText: 'Nhập email của bạn',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue, width: 1.0),
borderRadius: BorderRadius.circular(10),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.green, width: 2.0),
borderRadius: BorderRadius.circular(10),
),
),
keyboardType: TextInputType.emailAddress,
obscureText: false, // Đặt true cho mật khẩu
onChanged: (value) {
print('Giá trị thay đổi: $value');
},
onSubmitted: (value) {
print('Đã submit: $value');
},
)
Tạo bố cục trong Flutter thông qua các layout widget.
Widget linh hoạt nhất cho việc tạo box với khả năng tùy chỉnh cao:
Container(
width: 200,
height: 200,
margin: EdgeInsets.all(20),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 5,
blurRadius: 7,
offset: Offset(0, 3),
),
],
border: Border.all(color: Colors.blue, width: 2),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.blue.shade200, Colors.blue.shade800],
),
),
alignment: Alignment.center,
child: Text(
'Container',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
)
Sắp xếp các widget theo chiều ngang (Row) hoặc chiều dọc (Column):
// Row - sắp xếp theo chiều ngang
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // Phân bố không gian chính
crossAxisAlignment: CrossAxisAlignment.center, // Căn chỉnh trục chéo
children: [
Icon(Icons.star, size: 40),
Icon(Icons.star, size: 40),
Icon(Icons.star, size: 40),
],
)
// Column - sắp xếp theo chiều dọc
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, // Kéo dãn theo chiều ngang
children: [
Text('Item 1', textAlign: TextAlign.center),
SizedBox(height: 10), // Tạo khoảng cách
Text('Item 2', textAlign: TextAlign.center),
SizedBox(height: 10),
Text('Item 3', textAlign: TextAlign.center),
],
)
Xếp chồng các widget lên nhau:
Stack(
alignment: Alignment.center, // Căn chỉnh widget mặc định
children: [
// Widget đầu tiên ở dưới cùng
Container(
width: 300,
height: 200,
color: Colors.blue,
),
// Widget thứ hai ở giữa
Container(
width: 250,
height: 150,
color: Colors.red.withOpacity(0.7),
),
// Widget cuối cùng ở trên cùng
Positioned(
bottom: 20,
right: 20,
child: Container(
padding: EdgeInsets.all(10),
color: Colors.green,
child: Text(
'Positioned Widget',
style: TextStyle(color: Colors.white),
),
),
),
],
)
Kiểm soát cách widget chiếm không gian trong Row hoặc Column:
Row(
children: [
// Chiếm 1 phần không gian
Expanded(
flex: 1, // Tỷ lệ không gian (mặc định là 1)
child: Container(color: Colors.red, height: 100),
),
// Chiếm 2 phần không gian
Expanded(
flex: 2,
child: Container(color: Colors.green, height: 100),
),
// Giống Expanded nhưng cho phép thu nhỏ nếu cần
Flexible(
flex: 1,
fit: FlexFit.loose, // loose: có thể nhỏ hơn kích thước tối đa
child: Container(color: Colors.blue, height: 100, width: 50),
),
],
)
// Sử dụng Spacer để tạo khoảng trống
Row(
children: [
Text('Trái'),
Spacer(), // Chiếm tất cả không gian trống
Text('Phải'),
],
)
Hiển thị các widget theo dạng lưới:
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // Số cột
crossAxisSpacing: 10, // Khoảng cách giữa các cột
mainAxisSpacing: 10, // Khoảng cách giữa các hàng
childAspectRatio: 1, // Tỷ lệ chiều rộng/chiều cao của mỗi item
),
itemCount: 12,
itemBuilder: (context, index) {
return Container(
color: Colors.primaries[index % Colors.primaries.length],
child: Center(
child: Text(
'${index + 1}',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
);
},
)
Hiển thị danh sách các widget có thể cuộn:
// ListView.builder thích hợp cho danh sách dài
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.person),
title: Text('Người dùng ${index + 1}'),
subtitle: Text('Thông tin chi tiết'),
trailing: Icon(Icons.arrow_forward_ios),
onTap: () {
print('Đã nhấn vào item $index');
},
);
},
)
// ListView.separated thêm phần tử phân cách
ListView.separated(
itemCount: 20,
separatorBuilder: (context, index) => Divider(color: Colors.grey),
itemBuilder: (context, index) {
return ListTile(
title: Text('Item ${index + 1}'),
);
},
)
Tùy chỉnh hình dạng, màu sắc, đổ bóng và nhiều hiệu ứng khác:
Container(
decoration: BoxDecoration(
// Màu nền
color: Colors.white,
<span class="hljs-comment">// Hoặc gradient</span>
<span class="hljs-attribute">gradient</span>: LinearGradient(
<span class="hljs-attribute">colors</span>: [Colors.blue, Colors.purple],
<span class="hljs-attribute">begin</span>: Alignment.topLeft,
<span class="hljs-attribute">end</span>: Alignment.bottomRight,
),
<span class="hljs-comment">// Bo góc</span>
<span class="hljs-attribute">borderRadius</span>: BorderRadius.circular(<span class="hljs-number">12</span>),
<span class="hljs-comment">// Hoặc hình dạng tùy chỉnh</span>
<span class="hljs-attribute">shape</span>: BoxShape.circle, <span class="hljs-comment">// rectangle (mặc định) hoặc circle</span>
<span class="hljs-comment">// Viền</span>
<span class="hljs-attribute">border</span>: Border.all(
<span class="hljs-attribute">color</span>: Colors.blue,
<span class="hljs-attribute">width</span>: <span class="hljs-number">2</span>,
),
<span class="hljs-comment">// Đổ bóng</span>
<span class="hljs-attribute">boxShadow</span>: [
BoxShadow(
<span class="hljs-attribute">color</span>: Colors.grey.withOpacity(<span class="hljs-number">0.5</span>),
<span class="hljs-attribute">spreadRadius</span>: <span class="hljs-number">3</span>,
<span class="hljs-attribute">blurRadius</span>: <span class="hljs-number">7</span>,
<span class="hljs-attribute">offset</span>: Offset(<span class="hljs-number">0</span>, <span class="hljs-number">3</span>),
),
],
<span class="hljs-comment">// Hình ảnh nền</span>
<span class="hljs-attribute">image</span>: DecorationImage(
<span class="hljs-attribute">image</span>: NetworkImage(<span class="hljs-string">'https://example.com/image.jpg'</span>),
<span class="hljs-attribute">fit</span>: BoxFit.cover,
),
),
width: 200,
height: 200,
)
Cắt (clip) widget thành các hình dạng khác nhau:
// Bo tròn góc
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.network(
'https://example.com/image.jpg',
width: 200,
height: 200,
fit: BoxFit.cover,
),
)
// Hình tròn
ClipOval(
child: Image.network(
'https://example.com/image.jpg',
width: 100,
height: 100,
fit: BoxFit.cover,
),
)
// Hình dạng tùy chỉnh
ClipPath(
clipper: TriangleClipper(), // Custom clipper
child: Container(
color: Colors.blue,
height: 150,
width: 150,
),
)
// Custom Clipper
class TriangleClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.moveTo(size.width / 2, 0);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
return path;
}
@override
bool shouldReclip(TriangleClipper oldClipper) => false;
}
Thay đổi độ trong suốt của widget:
// Sử dụng widget Opacity
Opacity(
opacity: 0.5, // 0.0 hoàn toàn trong suốt, 1.0 hoàn toàn đục
child: Container(
color: Colors.blue,
width: 200,
height: 200,
),
)
// Hoặc sử dụng màu với alpha channel
Container(
color: Colors.red.withOpacity(0.7),
width: 200,
height: 200,
)
Tạo theme nhất quán cho toàn bộ ứng dụng:
MaterialApp(
theme: ThemeData(
// Màu cơ bản
primarySwatch: Colors.blue,
primaryColor: Colors.blue,
accentColor: Colors.orange,
<span class="hljs-comment">// Typography</span>
<span class="hljs-attribute">fontFamily</span>: <span class="hljs-string">'Roboto'</span>,
<span class="hljs-attribute">textTheme</span>: TextTheme(
<span class="hljs-attribute">headline1</span>: TextStyle(<span class="hljs-attribute">fontSize</span>: <span class="hljs-number">72.0</span>, <span class="hljs-attribute">fontWeight</span>: FontWeight.bold),
<span class="hljs-attribute">headline6</span>: TextStyle(<span class="hljs-attribute">fontSize</span>: <span class="hljs-number">36.0</span>, <span class="hljs-attribute">fontStyle</span>: FontStyle.italic),
<span class="hljs-attribute">bodyText2</span>: TextStyle(<span class="hljs-attribute">fontSize</span>: <span class="hljs-number">14.0</span>, <span class="hljs-attribute">fontFamily</span>: <span class="hljs-string">'Hind'</span>),
),
<span class="hljs-comment">// Component themes</span>
<span class="hljs-attribute">elevatedButtonTheme</span>: ElevatedButtonThemeData(
<span class="hljs-attribute">style</span>: ElevatedButton.styleFrom(
<span class="hljs-attribute">primary</span>: Colors.blue,
<span class="hljs-attribute">shape</span>: RoundedRectangleBorder(
<span class="hljs-attribute">borderRadius</span>: BorderRadius.circular(<span class="hljs-number">10</span>),
),
<span class="hljs-attribute">padding</span>: EdgeInsets.symmetric(<span class="hljs-attribute">horizontal</span>: <span class="hljs-number">20</span>, <span class="hljs-attribute">vertical</span>: <span class="hljs-number">12</span>),
),
),
<span class="hljs-attribute">appBarTheme</span>: AppBarTheme(
<span class="hljs-attribute">backgroundColor</span>: Colors.blue,
<span class="hljs-attribute">elevation</span>: <span class="hljs-number">0</span>,
<span class="hljs-attribute">titleTextStyle</span>: TextStyle(
<span class="hljs-attribute">fontWeight</span>: FontWeight.bold,
<span class="hljs-attribute">fontSize</span>: <span class="hljs-number">20</span>,
),
),
),
home: HomePage(),
)
Phát hiện nhiều loại cử chỉ khác nhau:
GestureDetector(
// Sự kiện nhấn
onTap: () {
print('Widget được nhấn');
},
onDoubleTap: () {
print('Widget được nhấn đúp');
},
onLongPress: () {
print('Widget được nhấn giữ');
},
// Sự kiện kéo
onPanUpdate: (details) {
print('Kéo: ${details.delta}');
},
// Sự kiện vuốt
onVerticalDragUpdate: (details) {
print('Vuốt dọc: ${details.delta.dy}');
},
onHorizontalDragUpdate: (details) {
print('Vuốt ngang: ${details.delta.dx}');
},
child: Container(
width: 200,
height: 200,
color: Colors.blue,
child: Center(
child: Text(
'Tương tác với tôi',
style: TextStyle(color: Colors.white),
),
),
),
)
Thêm hiệu ứng gợn sóng (ripple) khi tương tác:
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
print('InkWell được nhấn');
},
splashColor: Colors.blue.withOpacity(0.5),
highlightColor: Colors.blue.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
child: Container(
width: 200,
height: 100,
alignment: Alignment.center,
child: Text('Nhấn tôi để thấy hiệu ứng'),
),
),
)
Tạo hiệu ứng vuốt để xóa hoặc thực hiện hành động:
Dismissible(
key: Key('item-1'),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 20),
child: Icon(Icons.delete, color: Colors.white),
),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
print('Item đã bị xóa');
},
child: ListTile(
title: Text('Vuốt để xóa'),
subtitle: Text('Vuốt từ phải sang trái'),
),
)
Các component có sẵn với khả năng tương tác:
// Checkbox
StatefulBuilder(
builder: (context, setState) {
bool isChecked = false;
return Checkbox(
value: isChecked,
onChanged: (value) {
setState(() {
isChecked = value!;
});
},
);
}
)
// Radio
StatefulBuilder(
builder: (context, setState) {
int selectedValue = 1;
return Row(
children: [
Radio<int>(
value: 1,
groupValue: selectedValue,
onChanged: (value) {
setState(() {
selectedValue = value!;
});
},
),
Text('Tùy chọn 1'),
Radio<int>(
value: 2,
groupValue: selectedValue,
onChanged: (value) {
setState(() {
selectedValue = value!;
});
},
),
Text('Tùy chọn 2'),
],
);
}
)
// Switch
StatefulBuilder(
builder: (context, setState) {
bool isSwitched = false;
return Switch(
value: isSwitched,
onChanged: (value) {
setState(() {
isSwitched = value;
});
},
activeColor: Colors.blue,
);
}
)
// Slider
StatefulBuilder(
builder: (context, setState) {
double sliderValue = 0.5;
return Slider(
value: sliderValue,
onChanged: (value) {
setState(() {
sliderValue = value;
});
},
min: 0.0,
max: 1.0,
divisions: 10,
label: '${(sliderValue * 100).round()}%',
);
}
)
Sử dụng animated widgets có sẵn:
// Animated Container
StatefulBuilder(
builder: (context, setState) {
bool isExpanded = false;
return GestureDetector(
onTap: () {
setState(() {
isExpanded = !isExpanded;
});
},
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: isExpanded ? 200 : 100,
height: isExpanded ? 200 : 100,
color: isExpanded ? Colors.blue : Colors.red,
child: Center(
child: Text(
isExpanded ? 'Thu nhỏ' : 'Mở rộng',
style: TextStyle(color: Colors.white),
),
),
),
);
}
)
// AnimatedOpacity
StatefulBuilder(
builder: (context, setState) {
bool isVisible = true;
return Column(
children: [
ElevatedButton(
onPressed: () {
setState(() {
isVisible = !isVisible;
});
},
child: Text(isVisible ? 'Ẩn' : 'Hiện'),
),
AnimatedOpacity(
opacity: isVisible ? 1.0 : 0.0,
duration: Duration(milliseconds: 500),
child: Container(
width: 200,
height: 100,
color: Colors.green,
child: Center(
child: Text(
'Animation Opacity',
style: TextStyle(color: Colors.white),
),
),
),
),
],
);
}
)
Tạo animation tùy chỉnh:
class AnimationExample extends StatefulWidget {
@override
_AnimationExampleState createState() => _AnimationExampleState();
}
class _AnimationExampleState extends State<AnimationExample> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_animation = <span class="hljs-type">CurvedAnimation</span>(
parent: _controller,
curve: <span class="hljs-type">Curves</span>.elasticOut,
);
<span class="hljs-comment">// Lặp lại animation</span>
_controller.repeat(reverse: <span class="hljs-literal">true</span>);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _animation,
child: Container(
width: 100,
height: 100,
color: Colors.purple,
),
);
}
}
Tạo hiệu ứng chuyển tiếp mượt mà giữa các màn hình:
// Màn hình 1
Hero(
tag: 'imageHero', // Tag unique để liên kết với Hero ở màn hình khác
child: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
return DetailScreen();
}));
},
child: Image.network(
'https://example.com/image.jpg',
width: 100,
height: 100,
),
),
)
// Màn hình 2 (DetailScreen)
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Hero(
tag: 'imageHero', // Tag trùng với Hero ở màn hình 1
child: Image.network(
'https://example.com/image.jpg',
width: 300,
height: 300,
),
),
),
);
}
}
// Material App
MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Scaffold(
appBar: AppBar(
title: Text('Material App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Material Button
ElevatedButton(
onPressed: () {},
child: Text('Nhấn'),
),
// Material TextField
Padding(
<span class="hljs-name">padding</span>: EdgeInsets.all(<span class="hljs-number">16.0</span>),
child: TextField(
<span class="hljs-name">decoration</span>: InputDecoration(
<span class="hljs-name">labelText</span>: 'Nhập',
border: OutlineInputBorder(),
),
),
),
// Material Switch
Switch(
<span class="hljs-name">value</span>: true,
onChanged: (<span class="hljs-name">value</span>) {},
),
],
),
),
floatingActionButton: FloatingActionButton(
<span class="hljs-name">onPressed</span>: () {},
child: Icon(<span class="hljs-name">Icons</span>.add),
),
),
)
// Cupertino App
CupertinoApp(
theme: CupertinoThemeData(
primaryColor: CupertinoColors.activeBlue,
),
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Cupertino App'),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Cupertino Button
CupertinoButton(
onPressed: () {},
color: CupertinoColors.activeBlue,
child: Text('Nhấn'),
),
// Cupertino TextField
Padding(
<span class="hljs-name">padding</span>: EdgeInsets.all(<span class="hljs-number">16.0</span>),
child: CupertinoTextField(
<span class="hljs-name">placeholder</span>: 'Nhập',
padding: EdgeInsets.all(<span class="hljs-number">12.0</span>),
),
),
// Cupertino Switch
CupertinoSwitch(
<span class="hljs-name">value</span>: true,
onChanged: (<span class="hljs-name">value</span>) {},
),
],
),
),
),
)
Tự động thích ứng theo nền tảng:
// Widget thích ứng
class AdaptiveButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
AdaptiveButton({required this.text, required this.onPressed});
@override
Widget build(BuildContext context) {
final platform = Theme.of(context).platform;
<span class="hljs-keyword">if</span> (platform == <span class="hljs-type">TargetPlatform</span>.iOS || platform == <span class="hljs-type">TargetPlatform</span>.macOS) {
<span class="hljs-keyword">return</span> <span class="hljs-type">CupertinoButton</span>(
onPressed: onPressed,
color: <span class="hljs-type">CupertinoColors</span>.activeBlue,
child: <span class="hljs-type">Text</span>(text),
);
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">return</span> <span class="hljs-type">ElevatedButton</span>(
onPressed: onPressed,
child: <span class="hljs-type">Text</span>(text),
);
}
}
}
// Sử dụng
AdaptiveButton(
text: 'Nhấn',
onPressed: () {
print('Button được nhấn');
},
)
Tách logic và UI thành các widget riêng để tái sử dụng:
// StatelessWidget tùy chỉnh
class CustomCard extends StatelessWidget {
final String title;
final String description;
final String imageUrl;
final VoidCallback onTap;
CustomCard({
required this.title,
required this.description,
required this.imageUrl,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.0),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(12.0)),
child: Image.network(
imageUrl,
height: 120,
width: double.infinity,
fit: BoxFit.cover,
),
),
Padding(
padding: EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize:
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8.0),
Text(
description,
style: TextStyle(color: Colors.grey[700]),
),
],
),
),
],
),
),
);
}
}
// Sử dụng
CustomCard(
title: 'Tiêu đề bài viết',
description: 'Mô tả ngắn về bài viết này...',
imageUrl: 'https://example.com/image.jpg',
onTap: () {
print('Card được nhấn');
},
)
class CustomCounter extends StatefulWidget {
final int initialValue;
final ValueChanged<int>? onChanged;
CustomCounter({this.initialValue = 0, this.onChanged});
@override
_CustomCounterState createState() => _CustomCounterState();
}
class _CustomCounterState extends State<CustomCounter> {
late int _count;
@override
void initState() {
super.initState();
_count = widget.initialValue;
}
void _increment() {
setState(() {
_count++;
if (widget.onChanged != null) {
widget.onChanged!(_count);
}
});
}
void _decrement() {
setState(() {
_count--;
if (widget.onChanged != null) {
widget.onChanged!(_count);
}
});
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.remove),
onPressed: _count > 0 ? _decrement : null,
),
Container(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(4.0),
),
child: Text(
'$_count',
style: TextStyle(fontSize: 16.0),
),
),
IconButton(
icon: Icon(Icons.add),
onPressed: _increment,
),
],
);
}
}
// Sử dụng
CustomCounter(
initialValue: 5,
onChanged: (value) {
print('Giá trị hiện tại: $value');
},
)
Kết hợp nhiều widget để tạo giao diện phức tạp:
class ProductItem extends StatelessWidget {
final String name;
final String description;
final double price;
final String imageUrl;
final bool isAvailable;
ProductItem({
required this.name,
required this.description,
required this.price,
required this.imageUrl,
this.isAvailable = true,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Hình ảnh sản phẩm với badge
Stack(
children: [
Image.network(
imageUrl,
height: 180,
width: double.infinity,
fit: BoxFit.cover,
),
if (!isAvailable)
Positioned(
top: 12,
right: 12,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(8.0),
),
child: Text(
'Hết hàng',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
// <span class="hljs-type">Th</span>ông tin sản phẩm
<span class="hljs-type">Padding</span>(
<span class="hljs-title">padding</span>: <span class="hljs-type">EdgeInsets</span>.<span class="hljs-title">all</span>(16.0),
child: <span class="hljs-type">Column</span>(
<span class="hljs-title">crossAxisAlignment</span>: <span class="hljs-type">CrossAxisAlignment</span>.<span class="hljs-title">start</span>,
<span class="hljs-title">children</span>: [
<span class="hljs-type">Text</span>(
<span class="hljs-title">name</span>,
<span class="hljs-title">style</span>: <span class="hljs-type">TextStyle</span>(
<span class="hljs-title">fontSize</span>: 18,
<span class="hljs-title">fontWeight</span>: <span class="hljs-type">FontWeight</span>.<span class="hljs-title">bold</span>,
),
),
<span class="hljs-type">SizedBox</span>(<span class="hljs-title">height</span>: 8.0),
<span class="hljs-type">Text</span>(
<span class="hljs-title">description</span>,
<span class="hljs-title">style</span>: <span class="hljs-type">TextStyle</span>(
<span class="hljs-title">color</span>: <span class="hljs-type">Colors</span>.<span class="hljs-title">grey</span>[700],
<span class="hljs-title">fontSize</span>: 14,
),
maxLines: 2,
overflow: <span class="hljs-type">TextOverflow</span>.ellipsis,
),
<span class="hljs-type">SizedBox</span>(<span class="hljs-title">height</span>: 16.0),
<span class="hljs-type">Row</span>(
<span class="hljs-title">mainAxisAlignment</span>: <span class="hljs-type">MainAxisAlignment</span>.<span class="hljs-title">spaceBetween</span>,
<span class="hljs-title">children</span>: [
<span class="hljs-type">Text</span>(
'${<span class="hljs-title">price</span>.<span class="hljs-title">toStringAsFixed</span>(0)}đ',
style: <span class="hljs-type">TextStyle</span>(
<span class="hljs-title">fontSize</span>: 20,
<span class="hljs-title">fontWeight</span>: <span class="hljs-type">FontWeight</span>.<span class="hljs-title">bold</span>,
<span class="hljs-title">color</span>: <span class="hljs-type">Colors</span>.<span class="hljs-title">blue</span>,
),
),
<span class="hljs-type">ElevatedButton</span>(
<span class="hljs-title">onPressed</span>: <span class="hljs-title">isAvailable</span> ? () {} : null,
child: <span class="hljs-type">Text</span>('<span class="hljs-type">Th</span>ê<span class="hljs-title">m</span> <span class="hljs-title">v</span>à<span class="hljs-title">o</span> <span class="hljs-title">gi</span>ỏ'),
),
],
),
],
),
),
],
),
);
}
}
Tạo widget mới từ widget có sẵn:
// Widget kế thừa từ TextField
class RoundedTextField extends StatelessWidget {
final String hintText;
final IconData? prefixIcon;
final bool obscureText;
final TextEditingController? controller;
final String? Function(String?)? validator;
final void Function(String)? onChanged;
RoundedTextField({
required this.hintText,
this.prefixIcon,
this.obscureText = false,
this.controller,
this.validator,
this.onChanged,
});
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
obscureText: obscureText,
validator: validator,
onChanged: onChanged,
decoration: InputDecoration(
hintText: hintText,
filled: true,
fillColor: Colors.grey[200],
prefixIcon: prefixIcon != null ? Icon(prefixIcon) : null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30),
borderSide: BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30),
borderSide: BorderSide(color: Colors.red, width: 2),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
);
}
}
// Sử dụng
RoundedTextField(
hintText: 'Nhập email',
prefixIcon: Icons.email,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Vui lòng nhập email';
}
return null;
},
onChanged: (value) {
print('Email: $value');
},
)
// Tối ưu với const constructor
const Text(
'Hello Flutter',
style: const TextStyle(fontSize: 24),
)
// Không tối ưu
ListView(
children: List.generate(1000, (index) {
return ListTile(
title: Text('Item $index'),
);
}),
)
// Tối ưu
ListView.builder(
itemCount: 1000,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
)
class OptimizedCounter extends StatefulWidget {
@override
_OptimizedCounterState createState() => _OptimizedCounterState();
}
class _OptimizedCounterState extends State<OptimizedCounter> {
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
@override
void dispose() {
_counter.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
print('Rebuild toàn bộ widget'); // Chỉ chạy khi toàn bộ widget rebuild
<span class="hljs-keyword">return</span> <span class="hljs-type">Column</span>(
children: [
<span class="hljs-comment">// Chỉ widget này rebuild khi giá trị thay đổi</span>
<span class="hljs-type">ValueListenableBuilder</span><int>(
valueListenable: _counter,
builder: (context, value, child) {
print(<span class="hljs-symbol">'Rebuild</span> text'); <span class="hljs-comment">// Chỉ chạy khi value thay đổi</span>
<span class="hljs-keyword">return</span> <span class="hljs-type">Text</span>(
<span class="hljs-symbol">'Count</span>: $value',
style: <span class="hljs-type">TextStyle</span>(fontSize: <span class="hljs-number">24</span>),
);
},
),
<span class="hljs-type">ElevatedButton</span>(
onPressed: () {
_counter.value++;
},
child: <span class="hljs-type">Text</span>(<span class="hljs-symbol">'T</span>ăng'),
),
],
);
}
}
class ResponsiveWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Lấy thông tin về kích thước màn hình
final screenSize = MediaQuery.of(context).size;
final width = screenSize.width;
<span class="hljs-comment">// Responsive dựa trên chiều rộng màn hình</span>
<span class="hljs-keyword">if</span> (<span class="hljs-built_in">width</span> < <span class="hljs-number">600</span>) {
<span class="hljs-comment">// Mobile layout</span>
<span class="hljs-keyword">return</span> Column(
children: [
Container(
<span class="hljs-built_in">height</span>: <span class="hljs-number">200</span>,
<span class="hljs-built_in">color</span>: Colors.<span class="hljs-built_in">red</span>,
child: Center(child: Text(<span class="hljs-string">'Header'</span>)),
),
Container(
<span class="hljs-built_in">height</span>: <span class="hljs-number">300</span>,
<span class="hljs-built_in">color</span>: Colors.<span class="hljs-built_in">green</span>,
child: Center(child: Text(<span class="hljs-string">'Content'</span>)),
),
],
);
} <span class="hljs-keyword">else</span> {
<span class="hljs-comment">// Tablet/Desktop layout</span>
<span class="hljs-keyword">return</span> Row(
children: [
Container(
<span class="hljs-built_in">width</span>: <span class="hljs-built_in">width</span> * <span class="hljs-number">0.3</span>, <span class="hljs-comment">// 30% của màn hình</span>
<span class="hljs-built_in">color</span>: Colors.<span class="hljs-built_in">red</span>,
child: Center(child: Text(<span class="hljs-string">'Sidebar'</span>)),
),
Expanded(
child: Container(
<span class="hljs-built_in">color</span>: Colors.<span class="hljs-built_in">green</span>,
child: Center(child: Text(<span class="hljs-string">'Content'</span>)),
),
),
],
);
}
}
}
Duy trì tỷ lệ chiều rộng/chiều cao:
AspectRatio(
aspectRatio: 16 / 9, // Tỷ lệ 16:9
child: Container(
color: Colors.blue,
child: Center(
child: Text('Video Placeholder'),
),
),
)
Tạo layout phản ứng với kích thước của parent widget:
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
// Layout rộng
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.5,
),
itemCount: 12,
itemBuilder: (context, index) {
return Card(
child: Center(child: Text('Item $index')),
);
},
);
} else {
// Layout hẹp
return ListView.builder(
itemCount: 12,
itemBuilder: (context, index) {
return Card(
child: ListTile(title: Text('Item $index')),
);
},
);
}
},
)
Thích ứng với hướng màn hình (dọc/ngang):
OrientationBuilder(
builder: (context, orientation) {
return GridView.count(
// 2 cột khi ở chế độ dọc, 3 cột khi ở chế độ ngang
crossAxisCount: orientation == Orientation.portrait ? 2 : 3,
children: List.generate(9, (index) {
return Card(
color: Colors.primaries[index % Colors.primaries.length],
child: Center(child: Text('$index')),
);
}),
);
},
)
Xây dựng ứng dụng thời tiết đẹp sử dụng các widget và kỹ thuật đã học:
// Model
class WeatherData {
final String city;
final String condition;
final String icon;
final double temperature;
final double windSpeed;
final int humidity;
WeatherData({
required this.city,
required this.condition,
required this.icon,
required this.temperature,
required this.windSpeed,
required this.humidity,
});
}
// Mock Data
final mockWeather = WeatherData(
city: 'Hà Nội',
condition: 'Mưa nhẹ',
icon: '🌧️',
temperature: 28,
windSpeed: 5.2,
humidity: 75,
);
// UI
class WeatherApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.blue,
brightness: Brightness.dark,
),
home: WeatherScreen(),
);
}
}
class WeatherScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.blue.shade800, Colors.blue.shade200],
),
),
child: SafeArea(
child: Column(
children: [
// App Bar
Padding(
padding: EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
mockWeather.city,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
Text(
'${DateTime.now().day}/${DateTime.now().month}/${DateTime.now().year}',
style: TextStyle(
fontSize: 16,
),
),
],
),
IconButton(
icon: Icon(Icons.search, size: 30),
onPressed: () {},
),
],
),
),
// <span class="hljs-type">Weather</span> <span class="hljs-type">Icon</span> & <span class="hljs-type">Temperature</span>
<span class="hljs-type">Expanded</span>(
<span class="hljs-title">child</span>: <span class="hljs-type">Column</span>(
<span class="hljs-title">mainAxisAlignment</span>: <span class="hljs-type">MainAxisAlignment</span>.<span class="hljs-title">center</span>,
<span class="hljs-title">children</span>: [
<span class="hljs-type">Text</span>(
<span class="hljs-title">mockWeather</span>.<span class="hljs-title">icon</span>,
<span class="hljs-title">style</span>: <span class="hljs-type">TextStyle</span>(<span class="hljs-title">fontSize</span>: 80),
),
<span class="hljs-type">Text</span>(
'${<span class="hljs-title">mockWeather</span>.<span class="hljs-title">temperature</span>.<span class="hljs-title">round</span>()}°<span class="hljs-type">C'</span>,
style: <span class="hljs-type">TextStyle</span>(
<span class="hljs-title">fontSize</span>: 80,
<span class="hljs-title">fontWeight</span>: <span class="hljs-type">FontWeight</span>.<span class="hljs-title">bold</span>,
),
),
<span class="hljs-type">Text</span>(
<span class="hljs-title">mockWeather</span>.<span class="hljs-title">condition</span>,
<span class="hljs-title">style</span>: <span class="hljs-type">TextStyle</span>(<span class="hljs-title">fontSize</span>: 24),
),
],
),
),
// <span class="hljs-type">Weather</span> <span class="hljs-type">Details</span>
<span class="hljs-type">Container</span>(
<span class="hljs-title">margin</span>: <span class="hljs-type">EdgeInsets</span>.<span class="hljs-title">symmetric</span>(
<span class="hljs-title">horizontal</span>: 16,
<span class="hljs-title">vertical</span>: 24,
),
padding: <span class="hljs-type">EdgeInsets</span>.all(16),
decoration: <span class="hljs-type">BoxDecoration</span>(
<span class="hljs-title">color</span>: <span class="hljs-type">Colors</span>.<span class="hljs-title">white</span>.<span class="hljs-title">withOpacity</span>(0.2),
borderRadius: <span class="hljs-type">BorderRadius</span>.circular(16),
),
child: <span class="hljs-type">Row</span>(
<span class="hljs-title">mainAxisAlignment</span>: <span class="hljs-type">MainAxisAlignment</span>.<span class="hljs-title">spaceEvenly</span>,
<span class="hljs-title">children</span>: [
// <span class="hljs-type">Wind</span>
<span class="hljs-type">Column</span>(
<span class="hljs-title">children</span>: [
<span class="hljs-type">Icon</span>(<span class="hljs-type">Icons</span>.<span class="hljs-title">air</span>, <span class="hljs-title">size</span>: 30),
<span class="hljs-type">SizedBox</span>(<span class="hljs-title">height</span>: 8),
<span class="hljs-type">Text</span>(
'${<span class="hljs-title">mockWeather</span>.<span class="hljs-title">windSpeed</span>} <span class="hljs-title">km</span>/<span class="hljs-title">h'</span>,
<span class="hljs-title">style</span>: <span class="hljs-type">TextStyle</span>(<span class="hljs-title">fontSize</span>: 16),
),
<span class="hljs-type">Text</span>('<span class="hljs-type">Gi</span>ó'),
],
),
// <span class="hljs-type">Divider</span>
<span class="hljs-type">Container</span>(
<span class="hljs-title">height</span>: 50,
<span class="hljs-title">width</span>: 1,
<span class="hljs-title">color</span>: <span class="hljs-type">Colors</span>.<span class="hljs-title">white</span>,
),
// <span class="hljs-type">Humidity</span>
<span class="hljs-type">Column</span>(
<span class="hljs-title">children</span>: [
<span class="hljs-type">Icon</span>(<span class="hljs-type">Icons</span>.<span class="hljs-title">water_drop</span>, <span class="hljs-title">size</span>: 30),
<span class="hljs-type">SizedBox</span>(<span class="hljs-title">height</span>: 8),
<span class="hljs-type">Text</span>(
'${<span class="hljs-title">mockWeather</span>.<span class="hljs-title">humidity</span>}%',
<span class="hljs-title">style</span>: <span class="hljs-type">TextStyle</span>(<span class="hljs-title">fontSize</span>: 16),
),
<span class="hljs-type">Text</span>('Độ ẩ<span class="hljs-title">m'</span>),
],
),
],
),
),
// <span class="hljs-type">Forecast</span>
<span class="hljs-type">Container</span>(
<span class="hljs-title">height</span>: 120,
<span class="hljs-title">margin</span>: <span class="hljs-type">EdgeInsets</span>.<span class="hljs-title">only</span>(<span class="hljs-title">bottom</span>: 16),
child: <span class="hljs-type">ListView</span>.builder(
<span class="hljs-title">scrollDirection</span>: <span class="hljs-type">Axis</span>.<span class="hljs-title">horizontal</span>,
<span class="hljs-title">itemCount</span>: 7,
<span class="hljs-title">itemBuilder</span>: (<span class="hljs-title">context</span>, <span class="hljs-title">index</span>) {
final day = <span class="hljs-type">DateTime</span>.now().add(<span class="hljs-type">Duration</span>(<span class="hljs-title">days</span>: <span class="hljs-title">index</span> + 1));
final dayName = _getDayName(<span class="hljs-title">day</span>.<span class="hljs-title">weekday</span>);
return <span class="hljs-type">Container</span>(
<span class="hljs-title">width</span>: 80,
<span class="hljs-title">margin</span>: <span class="hljs-type">EdgeInsets</span>.<span class="hljs-title">symmetric</span>(<span class="hljs-title">horizontal</span>: 8),
padding: <span class="hljs-type">EdgeInsets</span>.all(8),
decoration: <span class="hljs-type">BoxDecoration</span>(
<span class="hljs-title">color</span>: <span class="hljs-type">Colors</span>.<span class="hljs-title">white</span>.<span class="hljs-title">withOpacity</span>(0.2),
borderRadius: <span class="hljs-type">BorderRadius</span>.circular(12),
),
child: <span class="hljs-type">Column</span>(
<span class="hljs-title">mainAxisAlignment</span>: <span class="hljs-type">MainAxisAlignment</span>.<span class="hljs-title">spaceEvenly</span>,
<span class="hljs-title">children</span>: [
<span class="hljs-type">Text(dayName)</span>,
<span class="hljs-type">Text</span>('🌤️'),
<span class="hljs-type">Text</span>('${(<span class="hljs-title">mockWeather</span>.<span class="hljs-title">temperature</span> - <span class="hljs-title">index</span>).round()}°<span class="hljs-type">C'</span>),
],
),
);
},
),
),
],
),
),
),
);
}
String _getDayName(int weekday) {
switch (weekday) {
case 1: return 'T2';
case 2: return 'T3';
case 3: return 'T4';
case 4: return 'T5';
case 5: return 'T6';
case 6: return 'T7';
case 7: return 'CN';
default: return '';
}
}
}
Vẽ đồ họa tùy chỉnh:
class CustomLoaderPainter extends CustomPainter {
final double progress;
final Color color;
CustomLoaderPainter({
required this.progress,
this.color = Colors.blue,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = min(size.width, size.height) / 2;
final paint = Paint()
..color = color.withOpacity(0.3)
..style = PaintingStyle.stroke
..strokeWidth = 10;
<span class="hljs-comment">// Vẽ vòng tròn nền</span>
canvas.drawCircle(center, radius, paint);
<span class="hljs-comment">// Vẽ phần progress</span>
<span class="hljs-keyword">final</span> progressPaint = <span class="hljs-type">Paint</span>()
..color = color
..style = <span class="hljs-type">PaintingStyle</span>.stroke
..strokeWidth = <span class="hljs-number">10</span>
..strokeCap = <span class="hljs-type">StrokeCap</span>.round;
canvas.drawArc(
<span class="hljs-type">Rect</span>.fromCircle(center: center, radius: radius),
-pi / <span class="hljs-number">2</span>, <span class="hljs-comment">// Bắt đầu từ đỉnh</span>
<span class="hljs-number">2</span> * pi * progress, <span class="hljs-comment">// Vẽ theo phần trăm progress</span>
<span class="hljs-literal">false</span>,
progressPaint,
);
}
@override
bool shouldRepaint(CustomLoaderPainter oldDelegate) {
return oldDelegate.progress != progress || oldDelegate.color != color;
}
}
// Sử dụng
class ProgressLoader extends StatefulWidget {
@override
_ProgressLoaderState createState() => _ProgressLoaderState();
}
class _ProgressLoaderState extends State<ProgressLoader> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..repeat();
_animation = <span class="hljs-type">Tween</span><double>(begin: <span class="hljs-number">0.0</span>, end: <span class="hljs-number">1.0</span>).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return CustomPaint(
painter: CustomLoaderPainter(
progress: _animation.value,
color: Colors.blue,
),
child: Container(
width: 100,
height: 100,
),
);
},
);
}
}
Tạo các hiệu ứng cuộn nâng cao:
CustomScrollView(
slivers: [
// App bar có thể co giãn khi cuộn
SliverAppBar(
expandedHeight: 200,
pinned: true, // AppBar cố định khi cuộn
flexibleSpace: FlexibleSpaceBar(
title: Text('Sliver App Bar'),
background: Image.network(
'https://example.com/image.jpg',
fit: BoxFit.cover,
),
),
),
// Tiêu đề danh sách
SliverToBoxAdapter(
<span class="hljs-name">child</span>: Padding(
<span class="hljs-name">padding</span>: EdgeInsets.all(<span class="hljs-number">16.0</span>),
child: Text(
'Danh sách các mục',
style: TextStyle(
<span class="hljs-name">fontSize</span>: <span class="hljs-number">20</span>,
fontWeight: FontWeight.bold,
),
),
),
),
// Danh sách items
SliverList(
<span class="hljs-name">delegate</span>: SliverChildBuilderDelegate(
(<span class="hljs-name">context</span>, index) {
return ListTile(
<span class="hljs-name">leading</span>: CircleAvatar(
<span class="hljs-name">child</span>: Text('${index + <span class="hljs-number">1</span>}'),
),
title: Text('Item ${index + <span class="hljs-number">1</span>}'),
subtitle: Text('Mô <span class="hljs-literal">t</span>ả item ${index + <span class="hljs-number">1</span>}'),
)<span class="hljs-comment">;</span>
},
childCount: <span class="hljs-number">20</span>,
),
),
// Grid view
SliverToBoxAdapter(
<span class="hljs-name">child</span>: Padding(
<span class="hljs-name">padding</span>: EdgeInsets.all(<span class="hljs-number">16.0</span>),
child: Text(
'Lưới các mục',
style: TextStyle(
<span class="hljs-name">fontSize</span>: <span class="hljs-number">20</span>,
fontWeight: FontWeight.bold,
),
),
),
),
SliverGrid(
<span class="hljs-name">gridDelegate</span>: SliverGridDelegateWithFixedCrossAxisCount(
<span class="hljs-name">crossAxisCount</span>: <span class="hljs-number">3</span>,
crossAxisSpacing: <span class="hljs-number">10</span>,
mainAxisSpacing: <span class="hljs-number">10</span>,
childAspectRatio: <span class="hljs-number">1</span>,
),
delegate: SliverChildBuilderDelegate(
(<span class="hljs-name">context</span>, index) {
return Container(
<span class="hljs-name">color</span>: Colors.primaries[index % Colors.primaries.length],
child: Center(
<span class="hljs-name">child</span>: Text('${index + <span class="hljs-number">1</span>}'),
),
)<span class="hljs-comment">;</span>
},
childCount: <span class="hljs-number">9</span>,
),
),
// Khoảng trống dưới cùng
SliverToBoxAdapter(
<span class="hljs-name">child</span>: SizedBox(<span class="hljs-name">height</span>: <span class="hljs-number">30</span>),
),
],
)
Các package UI phổ biến để tăng tốc quá trình phát triển:
dependencies:
flutter:
sdk: flutter
# UI Components và Utilities
flutter_staggered_grid_view: ^0.6.1 # Grid layout nâng cao
cached_network_image: ^3.2.0 # Tải và cache hình ảnh
flutter_svg: ^1.0.3 # Hỗ trợ SVG
shimmer: ^2.0.0 # Hiệu ứng loading shimmer
carousel_slider: ^4.0.0 # Slider/Carousel
flutter_slidable: ^1.2.0 # Swipe-to-action items
# Charts & Visualizations
fl_chart: ^0.40.0 # Biểu đồ đẹp và tùy biến
# Date/Time/Calendar
table_calendar: ^3.0.3 # Lịch tùy chỉnh
intl: ^0.17.0 # Định dạng ngày tháng, số,...
Các widget trong Flutter cung cấp sức mạnh và linh hoạt đáng kinh ngạc để xây dựng giao diện người dùng đẹp mắt và chuyên nghiệp. Bằng cách kết hợp các widgets, bạn có thể tạo ra vô số thiết kế khác nhau, từ đơn giản đến phức tạp.
Khi làm việc với UI trong Flutter, hãy nhớ các nguyên tắc sau:
const
constructor, ListView.builder và các kỹ thuật tối ưu khác.Với những kiến thức về Flutter widgets trong bài viết này, bạn đã có một nền tảng vững chắc để bắt đầu xây dựng các ứng dụng với giao diện đẹp mắt và chuyên nghiệp. Hãy tiếp tục thực hành và khám phá các khả năng vô tận mà Flutter mang lại!