Flutter: Dự án xây dựng UI cho ứng dụng
· 6 min read
Thiết kế giao diện người dùng (UI) là một phần quan trọng trong phát triển ứng dụng Flutter. Bài viết này sẽ hướng dẫn bạn cách xây dựng UI đẹp và hiệu quả cho ứng dụng Flutter.
1. Nguyên tắc thiết kế UI
1.1. Material Design
Material Design là hệ thống thiết kế của Google, cung cấp các nguyên tắc và thành phần để xây dựng giao diện người dùng đẹp và nhất quán.
MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
// Customize theme
textTheme: TextTheme(
headline1: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
bodyText1: TextStyle(fontSize: 16),
),
),
home: HomeScreen(),
)
1.2. Responsive Design
Thiết kế responsive giúp ứng dụng hoạt động tốt trên nhiều kích thước màn hình khác nhau.
class ResponsiveLayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
return DesktopLayout();
} else {
return MobileLayout();
}
},
);
}
}
2. Custom Widgets
2.1. Custom Button
class CustomButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
final Color? color;
const CustomButton({
Key? key,
required this.text,
required this.onPressed,
this.color,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
primary: color ?? Theme.of(context).primaryColor,
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
text,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
);
}
}
2.2. Custom Card
class CustomCard extends StatelessWidget {
final String title;
final String subtitle;
final Widget? trailing;
final VoidCallback? onTap;
const CustomCard({
Key? key,
required this.title,
required this.subtitle,
this.trailing,
this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.headline6,
),
SizedBox(height: 8),
Text(
subtitle,
style: Theme.of(context).textTheme.bodyText2,
),
if (trailing != null) ...[
SizedBox(height: 16),
trailing!,
],
],
),
),
),
);
}
}
3. Layout và Navigation
3.1. Bottom Navigation
class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _selectedIndex = 0;
final List<Widget> _screens = [
HomeScreen(),
SearchScreen(),
ProfileScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _screens[_selectedIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: 'Search',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profile',
),
],
),
);
}
}
3.2. Drawer Navigation
class DrawerScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Drawer Example'),
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
),
SizedBox(height: 16),
Text(
'John Doe',
style: TextStyle(
color: Colors.white,
fontSize: 18,
),
),
Text(
'john.doe@example.com',
style: TextStyle(
color: Colors.white70,
),
),
],
),
),
ListTile(
leading: Icon(Icons.home),
title: Text('Home'),
onTap: () {
Navigator.pop(context);
// Navigate to home
},
),
ListTile(
leading: Icon(Icons.settings),
title: Text('Settings'),
onTap: () {
Navigator.pop(context);
// Navigate to settings
},
),
],
),
),
body: Center(
child: Text('Main Content'),
),
);
}
}
4. Animations
4.1. Hero Animation
class HeroAnimation extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailScreen()),
);
},
child: Hero(
tag: 'imageHero',
child: Image.network(
'https://example.com/image.jpg',
width: 200,
height: 200,
),
),
);
}
}
4.2. Custom Animation
class AnimatedContainer extends StatefulWidget {
@override
_AnimatedContainerState createState() => _AnimatedContainerState();
}
class _AnimatedContainerState extends State<AnimatedContainer>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
_controller.repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: 1.0 + (_animation.value * 0.2),
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20),
),
),
);
},
);
}
}
5. Best Practices
5.1. Tổ chức code
lib/
├── widgets/
│ ├── custom_button.dart
│ ├── custom_card.dart
│ └── animated_container.dart
├── screens/
│ ├── home_screen.dart
│ └── detail_screen.dart
├── theme/
│ └── app_theme.dart
└── main.dart
5.2. Tối ưu hiệu suất
- Sử dụng
const
constructor khi có thể - Tránh rebuild không cần thiết
- Sử dụng
ListView.builder
thay vìListView
- Tối ưu hình ảnh và assets
// Good
const CustomWidget({
required this.title,
required this.onTap,
});
// Bad
CustomWidget({
required this.title,
required this.onTap,
});
6. Ví dụ thực tế
6.1. Profile Screen
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text('Profile'),
background: Image.network(
'https://example.com/cover.jpg',
fit: BoxFit.cover,
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
CircleAvatar(
radius: 50,
backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
),
SizedBox(height: 16),
Text(
'John Doe',
style: Theme.of(context).textTheme.headline5,
),
Text(
'Software Developer',
style: Theme.of(context).textTheme.subtitle1,
),
SizedBox(height: 24),
CustomButton(
text: 'Edit Profile',
onPressed: () {
// Navigate to edit profile
},
),
],
),
),
),
],
),
);
}
}
6.2. Settings Screen
class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Settings'),
),
body: ListView(
children: [
ListTile(
leading: Icon(Icons.notifications),
title: Text('Notifications'),
trailing: Switch(
value: true,
onChanged: (value) {
// Handle notification settings
},
),
),
ListTile(
leading: Icon(Icons.dark_mode),
title: Text('Dark Mode'),
trailing: Switch(
value: false,
onChanged: (value) {
// Handle theme settings
},
),
),
ListTile(
leading: Icon(Icons.language),
title: Text('Language'),
trailing: Text('English'),
onTap: () {
// Show language selection
},
),
],
),
);
}
}
Kết luận
Xây dựng UI trong Flutter là một quá trình sáng tạo và thú vị. Bằng cách tuân thủ các nguyên tắc thiết kế, sử dụng các widget có sẵn và tạo custom widget, bạn có thể xây dựng giao diện người dùng đẹp và hiệu quả.
Một số điểm cần lưu ý:
- Tuân thủ Material Design guidelines
- Tối ưu hiệu suất
- Tổ chức code rõ ràng
- Sử dụng animation một cách hợp lý
- Thiết kế responsive
Tài liệu tham khảo: