Skip to main content

9 posts tagged with "UI"

View All Tags

Flutter: Quản lý form và validation

· 8 min read

Form là một phần quan trọng trong hầu hết các ứng dụng. Bài viết này sẽ hướng dẫn cách quản lý form và thực hiện validation trong Flutter.

Form Validation

1. Form Widget

1.1. Cấu trúc cơ bản

class MyForm extends StatefulWidget {
@override
_MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
final _formKey = GlobalKey<FormState>();

@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(
labelText: 'Username',
hintText: 'Enter your username',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your username';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// Xử lý form
}
},
child: Text('Submit'),
),
],
),
);
}
}

1.2. FormField Widget

class CustomFormField extends FormField<String> {
CustomFormField({
Key? key,
required String label,
required String? Function(String?) validator,
void Function(String?)? onSaved,
}) : super(
key: key,
validator: validator,
onSaved: onSaved,
builder: (FormFieldState<String> state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label),
TextField(
onChanged: (value) {
state.didChange(value);
},
decoration: InputDecoration(
errorText: state.errorText,
),
),
],
);
},
);
}

2. Validation

2.1. Validation cơ bản

class ValidationForm extends StatefulWidget {
@override
_ValidationFormState createState() => _ValidationFormState();
}

class _ValidationFormState extends State<ValidationForm> {
final _formKey = GlobalKey<FormState>();
String _email = '';
String _password = '';

String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
if (!value.contains('@')) {
return 'Please enter a valid email';
}
return null;
}

String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
}

@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
validator: _validateEmail,
onSaved: (value) => _email = value ?? '',
),
TextFormField(
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
validator: _validatePassword,
onSaved: (value) => _password = value ?? '',
),
ElevatedButton(
onPressed: _submitForm,
child: Text('Submit'),
),
],
),
);
}

void _submitForm() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
// Xử lý form
}
}
}

2.2. Validation nâng cao

class AdvancedValidationForm extends StatefulWidget {
@override
_AdvancedValidationFormState createState() => _AdvancedValidationFormState();
}

class _AdvancedValidationFormState extends State<AdvancedValidationForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();

@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}

String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return 'Please enter a valid email';
}
return null;
}

String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 8) {
return 'Password must be at least 8 characters';
}
if (!value.contains(RegExp(r'[A-Z]'))) {
return 'Password must contain at least one uppercase letter';
}
if (!value.contains(RegExp(r'[0-9]'))) {
return 'Password must contain at least one number';
}
return null;
}

String? _validateConfirmPassword(String? value) {
if (value != _passwordController.text) {
return 'Passwords do not match';
}
return null;
}

@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
validator: _validateEmail,
),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
validator: _validatePassword,
),
TextFormField(
controller: _confirmPasswordController,
decoration: InputDecoration(
labelText: 'Confirm Password',
prefixIcon: Icon(Icons.lock_outline),
),
obscureText: true,
validator: _validateConfirmPassword,
),
ElevatedButton(
onPressed: _submitForm,
child: Text('Register'),
),
],
),
);
}

void _submitForm() {
if (_formKey.currentState!.validate()) {
// Xử lý form
}
}
}

3. Form với Provider

3.1. Form Provider

class FormProvider extends ChangeNotifier {
String _email = '';
String _password = '';
bool _isLoading = false;
String? _error;

String get email => _email;
String get password => _password;
bool get isLoading => _isLoading;
String? get error => _error;

void updateEmail(String value) {
_email = value;
notifyListeners();
}

void updatePassword(String value) {
_password = value;
notifyListeners();
}

Future<void> submitForm() async {
_isLoading = true;
_error = null;
notifyListeners();

try {
// Xử lý form
await Future.delayed(Duration(seconds: 1)); // Giả lập API call
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
}

3.2. Form với Provider

class ProviderForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => FormProvider(),
child: Consumer<FormProvider>(
builder: (context, formProvider, child) {
return Form(
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
onChanged: formProvider.updateEmail,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
return null;
},
),
TextFormField(
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
onChanged: formProvider.updatePassword,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
return null;
},
),
if (formProvider.error != null)
Text(
formProvider.error!,
style: TextStyle(color: Colors.red),
),
ElevatedButton(
onPressed: formProvider.isLoading
? null
: formProvider.submitForm,
child: formProvider.isLoading
? CircularProgressIndicator()
: Text('Submit'),
),
],
),
);
},
),
);
}
}

4. Best Practices

4.1. Tách biệt logic validation

class ValidationRules {
static String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return 'Please enter a valid email';
}
return null;
}

static String? validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 8) {
return 'Password must be at least 8 characters';
}
return null;
}
}

4.2. Xử lý lỗi và loading state

class FormState {
final bool isLoading;
final String? error;
final Map<String, String> fieldErrors;

FormState({
this.isLoading = false,
this.error,
this.fieldErrors = const {},
});

FormState copyWith({
bool? isLoading,
String? error,
Map<String, String>? fieldErrors,
}) {
return FormState(
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
fieldErrors: fieldErrors ?? this.fieldErrors,
);
}
}

5. Ví dụ thực tế

5.1. Form đăng ký

class RegistrationForm extends StatefulWidget {
@override
_RegistrationFormState createState() => _RegistrationFormState();
}

class _RegistrationFormState extends State<RegistrationForm> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;

@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}

Future<void> _submitForm() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});

try {
// Xử lý đăng ký
await Future.delayed(Duration(seconds: 1));
// Chuyển hướng sau khi đăng ký thành công
} catch (e) {
// Hiển thị lỗi
} finally {
setState(() {
_isLoading = false;
});
}
}
}

@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: InputDecoration(
labelText: 'Full Name',
prefixIcon: Icon(Icons.person),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Name is required';
}
return null;
},
),
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
validator: ValidationRules.validateEmail,
),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
validator: ValidationRules.validatePassword,
),
ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? CircularProgressIndicator()
: Text('Register'),
),
],
),
);
}
}

5.2. Form đăng nhập

class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
bool _obscurePassword = true;

@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}

Future<void> _submitForm() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});

try {
// Xử lý đăng nhập
await Future.delayed(Duration(seconds: 1));
// Chuyển hướng sau khi đăng nhập thành công
} catch (e) {
// Hiển thị lỗi
} finally {
setState(() {
_isLoading = false;
});
}
}
}

@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
validator: ValidationRules.validateEmail,
),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
obscureText: _obscurePassword,
validator: ValidationRules.validatePassword,
),
ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? CircularProgressIndicator()
: Text('Login'),
),
],
),
);
}
}

Kết luận

Quản lý form và validation là một phần quan trọng trong phát triển ứng dụng Flutter. Việc hiểu và áp dụng đúng cách các kỹ thuật quản lý form và validation sẽ giúp bạn tạo ra trải nghiệm người dùng tốt hơn và giảm thiểu lỗi trong ứng dụng.


Tài liệu tham khảo:

Flutter: Navigation và Routing cơ bản

· 4 min read

Navigation và Routing là những khái niệm 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 thực hiện điều hướng giữa các màn hình và quản lý route trong ứng dụng Flutter.

Navigation &amp; Routing

1. Navigation cơ bản

1.1. Điều hướng đơn giản

// Điều hướng đến màn hình mới
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);

// Quay lại màn hình trước
Navigator.pop(context);

1.2. Điều hướng với tham số

// Điều hướng và truyền tham số
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(item: item),
),
);

// Nhận tham số trong màn hình đích
class DetailScreen extends StatelessWidget {
final Item item;

const DetailScreen({Key? key, required this.item}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(item.title)),
body: Center(child: Text(item.description)),
);
}
}

2. Named Routes

2.1. Định nghĩa routes

MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/details': (context) => DetailScreen(),
'/settings': (context) => SettingsScreen(),
},
);

2.2. Điều hướng với named routes

// Điều hướng đến route đã đặt tên
Navigator.pushNamed(context, '/details');

// Điều hướng và truyền tham số
Navigator.pushNamed(
context,
'/details',
arguments: item,
);

// Nhận tham số trong màn hình đích
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final item = ModalRoute.of(context)!.settings.arguments as Item;
return Scaffold(
appBar: AppBar(title: Text(item.title)),
body: Center(child: Text(item.description)),
);
}
}

3. Nested Navigation

3.1. Bottom Navigation Bar

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. TabBar

class TabScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.home), text: 'Home'),
Tab(icon: Icon(Icons.search), text: 'Search'),
Tab(icon: Icon(Icons.person), text: 'Profile'),
],
),
),
body: TabBarView(
children: [
HomeScreen(),
SearchScreen(),
ProfileScreen(),
],
),
),
);
}
}

4. Best Practices

4.1. Quản lý Route

  • Sử dụng named routes cho các màn hình chính
  • Tổ chức routes theo cấu trúc rõ ràng
  • Xử lý các trường hợp route không tồn tại
MaterialApp(
onUnknownRoute: (settings) {
return MaterialPageRoute(
builder: (context) => NotFoundScreen(),
);
},
);

4.2. Xử lý Back Button

WillPopScope(
onWillPop: () async {
// Xử lý khi người dùng nhấn nút back
return await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Thoát?'),
content: Text('Bạn có muốn thoát không?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('Không'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('Có'),
),
],
),
);
},
child: Scaffold(
// ...
),
);

4.3. Deep Linking

MaterialApp(
onGenerateRoute: (settings) {
// Xử lý deep link
if (settings.name?.startsWith('/product/') ?? false) {
final productId = settings.name!.split('/').last;
return MaterialPageRoute(
builder: (context) => ProductScreen(id: productId),
);
}
return null;
},
);

5. Ví dụ thực tế

5.1. Ứng dụng E-commerce

class EcommerceApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/categories': (context) => CategoriesScreen(),
'/product': (context) => ProductScreen(),
'/cart': (context) => CartScreen(),
'/checkout': (context) => CheckoutScreen(),
},
);
}
}

5.2. Ứng dụng Social Media

class SocialApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => FeedScreen(),
'/profile': (context) => ProfileScreen(),
'/messages': (context) => MessagesScreen(),
'/notifications': (context) => NotificationsScreen(),
},
);
}
}

Kết luận

Navigation và Routing là những khái niệm cơ bản nhưng quan trọng trong phát triển ứng dụng Flutter. Việc hiểu rõ và áp dụng đúng cách sẽ giúp bạn tạo ra trải nghiệm người dùng tốt hơn.


Tài liệu tham khảo:

Flutter: Giới thiệu về Provider package

· 5 min read

Provider là một trong những package phổ biến nhất để quản lý state trong Flutter. Bài viết này sẽ giới thiệu về Provider và cách sử dụng nó hiệu quả.

Provider Package

1. Giới thiệu về Provider

Provider là một giải pháp quản lý state được phát triển bởi Remi Rousselet, một trong những core team member của Flutter. Provider được thiết kế để đơn giản hóa việc quản lý state và dependency injection trong ứng dụng Flutter.

1.1. Cài đặt Provider

Thêm Provider vào file pubspec.yaml:

dependencies:
provider: ^6.0.0

1.2. Các loại Provider

Provider cung cấp nhiều loại provider khác nhau:

  • Provider: Cung cấp một giá trị
  • ChangeNotifierProvider: Cung cấp một ChangeNotifier
  • ListenableProvider: Cung cấp một Listenable
  • ValueListenableProvider: Cung cấp một ValueListenable
  • StreamProvider: Cung cấp một Stream
  • FutureProvider: Cung cấp một Future

2. Sử dụng Provider

2.1. Tạo Provider

class CounterProvider extends ChangeNotifier {
int _count = 0;

int get count => _count;

void increment() {
_count++;
notifyListeners();
}

void decrement() {
_count--;
notifyListeners();
}
}

2.2. Cung cấp Provider

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: MaterialApp(
home: HomeScreen(),
),
);
}
}

2.3. Sử dụng Provider

class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Provider Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Consumer<CounterProvider>(
builder: (context, counter, child) {
return Text(
'Count: ${counter.count}',
style: Theme.of(context).textTheme.headline4,
);
},
),
ElevatedButton(
onPressed: () {
context.read<CounterProvider>().increment();
},
child: Text('Increment'),
),
],
),
),
);
}
}

3. Các Pattern phổ biến

3.1. MultiProvider

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CounterProvider()),
ChangeNotifierProvider(create: (_) => ThemeProvider()),
ChangeNotifierProvider(create: (_) => UserProvider()),
],
child: MaterialApp(
home: HomeScreen(),
),
);
}
}

3.2. ProxyProvider

class UserProvider extends ChangeNotifier {
String _name = '';
String get name => _name;

void updateName(String name) {
_name = name;
notifyListeners();
}
}

class UserProfileProvider extends ChangeNotifier {
final UserProvider userProvider;
String _greeting = '';

UserProfileProvider(this.userProvider) {
userProvider.addListener(_updateGreeting);
_updateGreeting();
}

String get greeting => _greeting;

void _updateGreeting() {
_greeting = 'Hello, ${userProvider.name}!';
notifyListeners();
}

@override
void dispose() {
userProvider.removeListener(_updateGreeting);
super.dispose();
}
}

// Sử dụng
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => UserProvider()),
ChangeNotifierProxyProvider<UserProvider, UserProfileProvider>(
create: (context) => UserProfileProvider(
Provider.of<UserProvider>(context, listen: false),
),
update: (context, userProvider, previous) =>
UserProfileProvider(userProvider),
),
],
child: MyApp(),
)

4. Best Practices

4.1. Tối ưu hóa hiệu suất

// Sử dụng Consumer thay vì Provider.of
Consumer<CounterProvider>(
builder: (context, counter, child) {
return Column(
children: [
Text('Count: ${counter.count}'),
child!, // Widget không thay đổi
],
);
},
child: const StaticWidget(), // Widget được tạo một lần
)

// Sử dụng Selector để chỉ rebuild khi cần
Selector<CounterProvider, int>(
selector: (_, provider) => provider.count,
builder: (context, count, child) {
return Text('Count: $count');
},
)

4.2. Xử lý lỗi

class DataProvider extends ChangeNotifier {
String? _error;
bool _isLoading = false;
List<Data> _data = [];

String? get error => _error;
bool get isLoading => _isLoading;
List<Data> get data => _data;

Future<void> fetchData() async {
try {
_isLoading = true;
_error = null;
notifyListeners();

_data = await api.fetchData();
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
}

5. Ví dụ thực tế

5.1. Ứng dụng Todo

class TodoProvider extends ChangeNotifier {
List<Todo> _todos = [];
bool _isLoading = false;

List<Todo> get todos => _todos;
bool get isLoading => _isLoading;

Future<void> addTodo(Todo todo) async {
_isLoading = true;
notifyListeners();

try {
final savedTodo = await api.saveTodo(todo);
_todos.add(savedTodo);
} finally {
_isLoading = false;
notifyListeners();
}
}

Future<void> toggleTodo(String id) async {
final todo = _todos.firstWhere((t) => t.id == id);
final updatedTodo = todo.copyWith(completed: !todo.completed);

try {
await api.updateTodo(updatedTodo);
final index = _todos.indexWhere((t) => t.id == id);
_todos[index] = updatedTodo;
notifyListeners();
} catch (e) {
// Xử lý lỗi
}
}
}

class TodoScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Todo List')),
body: Consumer<TodoProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return Center(child: CircularProgressIndicator());
}

return ListView.builder(
itemCount: provider.todos.length,
itemBuilder: (context, index) {
final todo = provider.todos[index];
return TodoItem(
todo: todo,
onToggle: () => provider.toggleTodo(todo.id),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Hiển thị dialog thêm todo mới
},
child: Icon(Icons.add),
),
);
}
}

5.2. Ứng dụng Authentication

class AuthProvider extends ChangeNotifier {
User? _user;
bool _isLoading = false;

User? get user => _user;
bool get isLoading => _isLoading;
bool get isAuthenticated => _user != null;

Future<void> login(String email, String password) async {
_isLoading = true;
notifyListeners();

try {
_user = await authService.login(email, password);
} finally {
_isLoading = false;
notifyListeners();
}
}

Future<void> logout() async {
_isLoading = true;
notifyListeners();

try {
await authService.logout();
_user = null;
} finally {
_isLoading = false;
notifyListeners();
}
}
}

class LoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: Consumer<AuthProvider>(
builder: (context, auth, child) {
if (auth.isLoading) {
return Center(child: CircularProgressIndicator());
}

return LoginForm(
onLogin: (email, password) {
auth.login(email, password);
},
);
},
),
);
}
}

Kết luận

Provider là một giải pháp quản lý state đơn giản nhưng mạnh mẽ trong Flutter. Nó giúp bạn tổ chức code tốt hơn, dễ bảo trì và mở rộng. Việc hiểu và sử dụng Provider đúng cách sẽ giúp bạn xây dựng ứng dụng Flutter hiệu quả hơn.


Tài liệu tham khảo:

Flutter: State trong StatefulWidget

· 5 min read

State là một khái niệm quan trọng trong Flutter, đặc biệt khi làm việc với StatefulWidget. Bài viết này sẽ giúp bạn hiểu rõ về cách quản lý state và các phương pháp hiệu quả.

StatefulWidget &amp; State

1. StatefulWidget và State

1.1. Cấu trúc cơ bản

class CounterWidget extends StatefulWidget {
@override
_CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Counter: $_counter'),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
);
}
}

1.2. Vòng đời của State

class _MyWidgetState extends State<MyWidget> {
@override
void initState() {
super.initState();
// Khởi tạo state
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
// Xử lý khi dependencies thay đổi
}

@override
void didUpdateWidget(MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// Xử lý khi widget được cập nhật
}

@override
void dispose() {
// Dọn dẹp tài nguyên
super.dispose();
}
}

2. Quản lý State

2.1. Local State

class _FormState extends State<FormWidget> {
final _formKey = GlobalKey<FormState>();
String _name = '';
String _email = '';

void _submitForm() {
if (_formKey.currentState!.validate()) {
setState(() {
// Cập nhật state
});
}
}

@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
onChanged: (value) {
setState(() {
_name = value;
});
},
),
TextFormField(
onChanged: (value) {
setState(() {
_email = value;
});
},
),
],
),
);
}
}

2.2. State với Animation

class _AnimatedWidgetState extends State<AnimatedWidget>
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,
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animation,
child: YourWidget(),
);
}
}

3. Best Practices

3.1. Tối ưu hóa setState

class _OptimizedWidgetState extends State<OptimizedWidget> {
int _counter = 0;
bool _isLoading = false;

void _updateCounter() {
// Chỉ cập nhật các widget cần thiết
setState(() {
_counter++;
});
}

void _loadData() async {
setState(() {
_isLoading = true;
});

// Thực hiện tác vụ bất đồng bộ
await Future.delayed(Duration(seconds: 1));

setState(() {
_isLoading = false;
});
}
}

3.2. Xử lý State phức tạp

class _ComplexWidgetState extends State<ComplexWidget> {
// Sử dụng các biến riêng biệt cho từng loại state
int _counter = 0;
String _selectedItem = '';
List<String> _items = [];
bool _isLoading = false;

// Tách logic xử lý state
void _handleCounterChange() {
setState(() {
_counter++;
});
}

void _handleItemSelection(String item) {
setState(() {
_selectedItem = item;
});
}

Future<void> _loadItems() async {
setState(() {
_isLoading = true;
});

try {
final items = await fetchItems();
setState(() {
_items = items;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
// Xử lý lỗi
}
}
}

4. Ví dụ thực tế

4.1. Form với Validation

class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
String _email = '';
String _password = '';
bool _isLoading = false;

Future<void> _submitForm() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});

try {
await login(_email, _password);
// Xử lý đăng nhập thành công
} catch (e) {
// Xử lý lỗi
} finally {
setState(() {
_isLoading = false;
});
}
}
}

@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
onChanged: (value) => _email = value,
validator: (value) {
if (value?.isEmpty ?? true) {
return 'Please enter your email';
}
return null;
},
),
TextFormField(
onChanged: (value) => _password = value,
obscureText: true,
validator: (value) {
if (value?.isEmpty ?? true) {
return 'Please enter your password';
}
return null;
},
),
ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? CircularProgressIndicator()
: Text('Login'),
),
],
),
);
}
}

4.2. List với CRUD Operations

class _TodoListState extends State<TodoList> {
List<Todo> _todos = [];
bool _isLoading = false;

Future<void> _loadTodos() async {
setState(() {
_isLoading = true;
});

try {
final todos = await fetchTodos();
setState(() {
_todos = todos;
});
} finally {
setState(() {
_isLoading = false;
});
}
}

Future<void> _addTodo(Todo todo) async {
setState(() {
_todos.add(todo);
});
await saveTodo(todo);
}

Future<void> _updateTodo(Todo todo) async {
setState(() {
final index = _todos.indexWhere((t) => t.id == todo.id);
if (index != -1) {
_todos[index] = todo;
}
});
await updateTodo(todo);
}

Future<void> _deleteTodo(String id) async {
setState(() {
_todos.removeWhere((todo) => todo.id == id);
});
await deleteTodo(id);
}
}

Kết luận

Quản lý state trong StatefulWidget là một kỹ năng quan trọng trong phát triển ứng dụng Flutter. Việc hiểu rõ và áp dụng đúng cách sẽ giúp bạn tạo ra ứng dụng có hiệu suất tốt và dễ bảo trì.


Tài liệu tham khảo:

Flutter: Xây dựng chức năng quản lý Tasks

· 3 min read

Quản lý tasks là một chức năng phổ biến trong các ứng dụng quản lý công việc, Todo List, dự án... Bài viết này hướng dẫn bạn xây dựng chức năng quản lý tasks hoàn chỉnh trong Flutter, sử dụng Provider để quản lý trạng thái và lưu trữ dữ liệu.

1. Mô hình dữ liệu Task

class Task {
final String id;
String title;
String description;
DateTime? deadline;
bool isCompleted;

Task({
required this.id,
required this.title,
this.description = '',
this.deadline,
this.isCompleted = false,
});
}

2. Provider quản lý danh sách tasks

import 'package:flutter/material.dart';

class TaskProvider with ChangeNotifier {
List<Task> _tasks = [];

List<Task> get tasks => _tasks;

void addTask(Task task) {
_tasks.add(task);
notifyListeners();
}

void updateTask(Task task) {
final index = _tasks.indexWhere((t) => t.id == task.id);
if (index != -1) {
_tasks[index] = task;
notifyListeners();
}
}

void deleteTask(String id) {
_tasks.removeWhere((t) => t.id == id);
notifyListeners();
}

void toggleComplete(String id) {
final index = _tasks.indexWhere((t) => t.id == id);
if (index != -1) {
_tasks[index].isCompleted = !_tasks[index].isCompleted;
notifyListeners();
}
}
}

3. Giao diện quản lý tasks

Hiển thị danh sách tasks

Consumer<TaskProvider>(
builder: (context, taskProvider, child) {
final tasks = taskProvider.tasks;
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return ListTile(
title: Text(task.title),
subtitle: Text(task.description),
trailing: Checkbox(
value: task.isCompleted,
onChanged: (_) => taskProvider.toggleComplete(task.id),
),
onTap: () {
// Chỉnh sửa task
},
onLongPress: () {
// Xóa task
taskProvider.deleteTask(task.id);
},
);
},
);
},
)

Thêm task mới

void _addTask(BuildContext context) {
final provider = Provider.of<TaskProvider>(context, listen: false);
final newTask = Task(
id: UniqueKey().toString(),
title: 'Task mới',
description: 'Mô tả...',
deadline: DateTime.now().add(Duration(days: 1)),
);
provider.addTask(newTask);
}

4. Lưu trữ tasks (local storage)

Bạn có thể sử dụng shared_preferences, hive hoặc sqflite để lưu trữ tasks. Ví dụ với shared_preferences:

import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

Future<void> saveTasks(List<Task> tasks) async {
final prefs = await SharedPreferences.getInstance();
final tasksJson = jsonEncode(tasks.map((t) => {
'id': t.id,
'title': t.title,
'description': t.description,
'deadline': t.deadline?.toIso8601String(),
'isCompleted': t.isCompleted,
}).toList());
await prefs.setString('tasks', tasksJson);
}

5. Hình minh họa kiến trúc quản lý tasks

Task Manager Architecture

6. Best Practices

  • Sử dụng Provider hoặc Riverpod để quản lý state.
  • Tách biệt logic và UI.
  • Lưu trữ dữ liệu local hoặc cloud.
  • Sử dụng UUID cho id task.
  • Thêm xác nhận khi xóa task.

7. Tài liệu tham khảo

Flutter: Dự án thiết kế ứng dụng Todo List

· 8 min read

Todo List là một ứng dụng phổ biến và là dự án tuyệt vời để học Flutter. Bài viết này sẽ hướng dẫn bạn xây dựng một ứng dụng Todo List hoàn chỉnh với các tính năng cơ bản và nâng cao.

Todo List App

1. Cấu trúc dự án

1.1. Cấu trúc thư mục

lib/
├── models/
│ └── todo.dart
├── providers/
│ └── todo_provider.dart
├── screens/
│ ├── home_screen.dart
│ └── add_todo_screen.dart
├── widgets/
│ ├── todo_item.dart
│ └── todo_list.dart
└── main.dart

1.2. Cài đặt dependencies

dependencies:
flutter:
sdk: flutter
provider: ^6.0.0
sqflite: ^2.0.0
path: ^1.8.0
intl: ^0.17.0

2. Model

2.1. Todo Model

class Todo {
final int? id;
final String title;
final String description;
final DateTime dueDate;
final bool isCompleted;
final Priority priority;

Todo({
this.id,
required this.title,
required this.description,
required this.dueDate,
this.isCompleted = false,
this.priority = Priority.medium,
});

Todo copyWith({
int? id,
String? title,
String? description,
DateTime? dueDate,
bool? isCompleted,
Priority? priority,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
dueDate: dueDate ?? this.dueDate,
isCompleted: isCompleted ?? this.isCompleted,
priority: priority ?? this.priority,
);
}

Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'description': description,
'dueDate': dueDate.toIso8601String(),
'isCompleted': isCompleted ? 1 : 0,
'priority': priority.index,
};
}

factory Todo.fromMap(Map<String, dynamic> map) {
return Todo(
id: map['id'],
title: map['title'],
description: map['description'],
dueDate: DateTime.parse(map['dueDate']),
isCompleted: map['isCompleted'] == 1,
priority: Priority.values[map['priority']],
);
}
}

enum Priority { low, medium, high }

3. Provider

3.1. Todo Provider

class TodoProvider extends ChangeNotifier {
List<Todo> _todos = [];
bool _isLoading = false;
String? _error;

List<Todo> get todos => _todos;
bool get isLoading => _isLoading;
String? get error => _error;

Future<void> loadTodos() async {
_isLoading = true;
_error = null;
notifyListeners();

try {
final db = await DatabaseHelper.instance.database;
final List<Map<String, dynamic>> maps = await db.query('todos');
_todos = maps.map((map) => Todo.fromMap(map)).toList();
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}

Future<void> addTodo(Todo todo) async {
try {
final db = await DatabaseHelper.instance.database;
final id = await db.insert('todos', todo.toMap());
_todos.add(todo.copyWith(id: id));
notifyListeners();
} catch (e) {
_error = e.toString();
notifyListeners();
}
}

Future<void> updateTodo(Todo todo) async {
try {
final db = await DatabaseHelper.instance.database;
await db.update(
'todos',
todo.toMap(),
where: 'id = ?',
whereArgs: [todo.id],
);
final index = _todos.indexWhere((t) => t.id == todo.id);
_todos[index] = todo;
notifyListeners();
} catch (e) {
_error = e.toString();
notifyListeners();
}
}

Future<void> deleteTodo(int id) async {
try {
final db = await DatabaseHelper.instance.database;
await db.delete(
'todos',
where: 'id = ?',
whereArgs: [id],
);
_todos.removeWhere((todo) => todo.id == id);
notifyListeners();
} catch (e) {
_error = e.toString();
notifyListeners();
}
}
}

3.2. Database Helper

class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;

DatabaseHelper._init();

Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('todos.db');
return _database!;
}

Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, filePath);

return await openDatabase(
path,
version: 1,
onCreate: _createDB,
);
}

Future<void> _createDB(Database db, int version) async {
await db.execute('''
CREATE TABLE todos(
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
dueDate TEXT NOT NULL,
isCompleted INTEGER NOT NULL,
priority INTEGER NOT NULL
)
''');
}
}

4. UI Components

4.1. Todo Item Widget

class TodoItem extends StatelessWidget {
final Todo todo;
final VoidCallback onToggle;
final VoidCallback onDelete;

const TodoItem({
Key? key,
required this.todo,
required this.onToggle,
required this.onDelete,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) => onToggle(),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted ? TextDecoration.lineThrough : null,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(todo.description),
SizedBox(height: 4),
Text(
'Due: ${DateFormat('MMM dd, yyyy').format(todo.dueDate)}',
style: TextStyle(
color: todo.dueDate.isBefore(DateTime.now())
? Colors.red
: Colors.grey,
),
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildPriorityIndicator(),
IconButton(
icon: Icon(Icons.delete),
onPressed: onDelete,
),
],
),
),
);
}

Widget _buildPriorityIndicator() {
Color color;
switch (todo.priority) {
case Priority.high:
color = Colors.red;
break;
case Priority.medium:
color = Colors.orange;
break;
case Priority.low:
color = Colors.green;
break;
}
return Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
);
}
}

4.2. Todo List Widget

class TodoList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<TodoProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return Center(child: CircularProgressIndicator());
}

if (provider.error != null) {
return Center(
child: Text(
'Error: ${provider.error}',
style: TextStyle(color: Colors.red),
),
);
}

if (provider.todos.isEmpty) {
return Center(
child: Text('No todos yet. Add one!'),
);
}

return ListView.builder(
itemCount: provider.todos.length,
itemBuilder: (context, index) {
final todo = provider.todos[index];
return TodoItem(
todo: todo,
onToggle: () {
provider.updateTodo(
todo.copyWith(isCompleted: !todo.isCompleted),
);
},
onDelete: () {
provider.deleteTodo(todo.id!);
},
);
},
);
},
);
}
}

5. Screens

5.1. Home Screen

class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Todo List'),
actions: [
IconButton(
icon: Icon(Icons.sort),
onPressed: () {
// Hiển thị dialog sắp xếp
},
),
],
),
body: TodoList(),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => AddTodoScreen()),
);
},
child: Icon(Icons.add),
),
);
}
}

5.2. Add Todo Screen

class AddTodoScreen extends StatefulWidget {
@override
_AddTodoScreenState createState() => _AddTodoScreenState();
}

class _AddTodoScreenState extends State<AddTodoScreen> {
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
DateTime _dueDate = DateTime.now();
Priority _priority = Priority.medium;

@override
void dispose() {
_titleController.dispose();
_descriptionController.dispose();
super.dispose();
}

Future<void> _selectDate() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _dueDate,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(Duration(days: 365)),
);
if (picked != null && picked != _dueDate) {
setState(() {
_dueDate = picked;
});
}
}

void _submitForm() {
if (_formKey.currentState!.validate()) {
final todo = Todo(
title: _titleController.text,
description: _descriptionController.text,
dueDate: _dueDate,
priority: _priority,
);

context.read<TodoProvider>().addTodo(todo);
Navigator.pop(context);
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Add Todo'),
),
body: Form(
key: _formKey,
child: ListView(
padding: EdgeInsets.all(16),
children: [
TextFormField(
controller: _titleController,
decoration: InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a title';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
controller: _descriptionController,
decoration: InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
),
maxLines: 3,
),
SizedBox(height: 16),
ListTile(
title: Text('Due Date'),
subtitle: Text(
DateFormat('MMM dd, yyyy').format(_dueDate),
),
trailing: Icon(Icons.calendar_today),
onTap: _selectDate,
),
SizedBox(height: 16),
DropdownButtonFormField<Priority>(
value: _priority,
decoration: InputDecoration(
labelText: 'Priority',
border: OutlineInputBorder(),
),
items: Priority.values.map((priority) {
return DropdownMenuItem(
value: priority,
child: Text(priority.toString().split('.').last),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_priority = value;
});
}
},
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _submitForm,
child: Text('Add Todo'),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
),
),
],
),
),
);
}
}

6. Main App

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TodoProvider(),
child: MaterialApp(
title: 'Todo List',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomeScreen(),
),
);
}
}

7. Tính năng nâng cao

7.1. Sắp xếp và lọc

enum SortOption { dueDate, priority, title }

class TodoProvider extends ChangeNotifier {
// ... existing code ...

SortOption _sortOption = SortOption.dueDate;
bool _showCompleted = true;

List<Todo> get filteredTodos {
var todos = _todos;

if (!_showCompleted) {
todos = todos.where((todo) => !todo.isCompleted).toList();
}

switch (_sortOption) {
case SortOption.dueDate:
todos.sort((a, b) => a.dueDate.compareTo(b.dueDate));
break;
case SortOption.priority:
todos.sort((a, b) => b.priority.index.compareTo(a.priority.index));
break;
case SortOption.title:
todos.sort((a, b) => a.title.compareTo(b.title));
break;
}

return todos;
}

void setSortOption(SortOption option) {
_sortOption = option;
notifyListeners();
}

void toggleShowCompleted() {
_showCompleted = !_showCompleted;
notifyListeners();
}
}

7.2. Thông báo

class NotificationService {
static Future<void> scheduleNotification(Todo todo) async {
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();

const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');

final InitializationSettings initializationSettings =
InitializationSettings(android: initializationSettingsAndroid);

await flutterLocalNotificationsPlugin.initialize(initializationSettings);

await flutterLocalNotificationsPlugin.zonedSchedule(
todo.id!,
'Todo Reminder',
todo.title,
TZDateTime.from(todo.dueDate, local),
NotificationDetails(
android: AndroidNotificationDetails(
'todo_reminders',
'Todo Reminders',
importance: Importance.high,
priority: Priority.high,
),
),
androidAllowWhileIdle: true,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
);
}
}

Kết luận

Ứng dụng Todo List là một dự án tuyệt vời để học Flutter. Nó bao gồm nhiều khái niệm quan trọng như:

  • Quản lý state với Provider
  • Lưu trữ dữ liệu local với SQLite
  • Xây dựng UI phức tạp
  • Xử lý form và validation
  • Quản lý thông báo

Bạn có thể mở rộng dự án này bằng cách thêm các tính năng như:

  • Đồng bộ hóa với backend
  • Phân loại todo theo danh mục
  • Tìm kiếm và lọc nâng cao
  • Giao diện người dùng tùy chỉnh
  • Hỗ trợ đa ngôn ngữ

Tài liệu tham khảo:

Flutter: Truyền dữ liệu giữa các widget

· 5 min read

Truyền dữ liệu giữa các widget là một khía cạnh quan trọng trong phát triển ứng dụng Flutter. Bài viết này sẽ giới thiệu các phương pháp khác nhau để truyền dữ liệu giữa các widget.

Widget Data Passing

1. Truyền dữ liệu qua Constructor

1.1. Truyền dữ liệu đơn giản

class ParentWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChildWidget(
title: 'Hello',
count: 42,
);
}
}

class ChildWidget extends StatelessWidget {
final String title;
final int count;

const ChildWidget({
Key? key,
required this.title,
required this.count,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return Column(
children: [
Text(title),
Text('Count: $count'),
],
);
}
}

1.2. Truyền Callback Functions

class ParentWidget extends StatelessWidget {
void _handleButtonPress() {
print('Button pressed!');
}

@override
Widget build(BuildContext context) {
return ChildWidget(
onButtonPressed: _handleButtonPress,
);
}
}

class ChildWidget extends StatelessWidget {
final VoidCallback onButtonPressed;

const ChildWidget({
Key? key,
required this.onButtonPressed,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onButtonPressed,
child: Text('Press Me'),
);
}
}

2. InheritedWidget

2.1. Tạo InheritedWidget

class UserData extends InheritedWidget {
final String username;
final String email;

const UserData({
Key? key,
required this.username,
required this.email,
required Widget child,
}) : super(key: key, child: child);

static UserData of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<UserData>()!;
}

@override
bool updateShouldNotify(UserData oldWidget) {
return username != oldWidget.username || email != oldWidget.email;
}
}

2.2. Sử dụng InheritedWidget

class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return UserData(
username: 'john_doe',
email: 'john@example.com',
child: MaterialApp(
home: HomeScreen(),
),
);
}
}

class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final userData = UserData.of(context);
return Column(
children: [
Text('Username: ${userData.username}'),
Text('Email: ${userData.email}'),
],
);
}
}

3. Provider Package

3.1. Tạo Provider

class UserProvider extends ChangeNotifier {
String _username = '';
String _email = '';

String get username => _username;
String get email => _email;

void updateUser(String username, String email) {
_username = username;
_email = email;
notifyListeners();
}
}

3.2. Sử dụng Provider

class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => UserProvider(),
child: MaterialApp(
home: HomeScreen(),
),
);
}
}

class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final userProvider = Provider.of<UserProvider>(context);
return Column(
children: [
Text('Username: ${userProvider.username}'),
Text('Email: ${userProvider.email}'),
ElevatedButton(
onPressed: () {
userProvider.updateUser('new_user', 'new@example.com');
},
child: Text('Update User'),
),
],
);
}
}

4. Stream và StreamBuilder

4.1. Tạo Stream

class DataStream {
final _controller = StreamController<String>();

Stream<String> get stream => _controller.stream;

void addData(String data) {
_controller.add(data);
}

void dispose() {
_controller.close();
}
}

4.2. Sử dụng StreamBuilder

class StreamWidget extends StatelessWidget {
final DataStream dataStream;

const StreamWidget({
Key? key,
required this.dataStream,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return StreamBuilder<String>(
stream: dataStream.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('Data: ${snapshot.data}');
}
return CircularProgressIndicator();
},
);
}
}

5. Best Practices

5.1. Chọn phương pháp phù hợp

  • Constructor: Cho dữ liệu đơn giản và tĩnh
  • InheritedWidget: Cho dữ liệu được chia sẻ rộng rãi
  • Provider: Cho state management phức tạp
  • Stream: Cho dữ liệu thay đổi theo thời gian thực

5.2. Tối ưu hóa hiệu suất

// Sử dụng const constructor khi có thể
const ChildWidget({
required this.title,
required this.count,
});

// Tránh rebuild không cần thiết
class OptimizedWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<UserProvider>(
builder: (context, userProvider, child) {
return Column(
children: [
// Widget thay đổi
Text(userProvider.username),
// Widget không thay đổi
child!,
],
);
},
child: const StaticWidget(),
);
}
}

6. Ví dụ thực tế

6.1. Ứng dụng Todo

class TodoProvider extends ChangeNotifier {
List<Todo> _todos = [];

List<Todo> get todos => _todos;

void addTodo(Todo todo) {
_todos.add(todo);
notifyListeners();
}

void removeTodo(String id) {
_todos.removeWhere((todo) => todo.id == id);
notifyListeners();
}
}

class TodoList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<TodoProvider>(
builder: (context, todoProvider, child) {
return ListView.builder(
itemCount: todoProvider.todos.length,
itemBuilder: (context, index) {
final todo = todoProvider.todos[index];
return TodoItem(todo: todo);
},
);
},
);
}
}

6.2. Ứng dụng Chat

class ChatProvider extends ChangeNotifier {
final _messages = <Message>[];
final _streamController = StreamController<Message>();

Stream<Message> get messageStream => _streamController.stream;

void sendMessage(Message message) {
_messages.add(message);
_streamController.add(message);
notifyListeners();
}

@override
void dispose() {
_streamController.close();
super.dispose();
}
}

class ChatScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: StreamBuilder<Message>(
stream: context.read<ChatProvider>().messageStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return MessageBubble(message: snapshot.data!);
}
return Container();
},
),
),
MessageInput(
onSend: (text) {
context.read<ChatProvider>().sendMessage(
Message(text: text, timestamp: DateTime.now()),
);
},
),
],
);
}
}

Kết luận

Việc chọn phương pháp truyền dữ liệu phù hợp là rất quan trọng trong phát triển ứng dụng Flutter. Mỗi phương pháp có ưu điểm và trường hợp sử dụng riêng. Hiểu rõ các phương pháp này sẽ giúp bạn tạo ra ứng dụng có hiệu suất tốt và dễ bảo trì.


Tài liệu tham khảo:

Flutter: ListView và GridView widgets

· 4 min read

ListView và GridView là hai widget quan trọng trong Flutter để hiển thị dữ liệu dạng danh sách và lưới. Bài viết này sẽ hướng dẫn bạn cách sử dụng chúng một cách hiệu quả.

1. ListView Widget

ListView là widget được sử dụng để hiển thị danh sách các phần tử theo chiều dọc hoặc ngang.

ListView Widget Examples

1.1. Basic ListView

ListView(
children: [
ListTile(
leading: Icon(Icons.star),
title: Text('Item 1'),
subtitle: Text('Description 1'),
),
ListTile(
leading: Icon(Icons.star),
title: Text('Item 2'),
subtitle: Text('Description 2'),
),
ListTile(
leading: Icon(Icons.star),
title: Text('Item 3'),
subtitle: Text('Description 3'),
),
],
)

1.2. ListView.builder

ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.star),
title: Text(items[index].title),
subtitle: Text(items[index].description),
);
},
)

1.3. ListView.separated

ListView.separated(
itemCount: items.length,
separatorBuilder: (context, index) => Divider(),
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.star),
title: Text(items[index].title),
subtitle: Text(items[index].description),
);
},
)

1.4. Horizontal ListView

ListView(
scrollDirection: Axis.horizontal,
children: [
Container(
width: 160,
margin: EdgeInsets.all(8),
color: Colors.blue,
child: Center(child: Text('Item 1')),
),
Container(
width: 160,
margin: EdgeInsets.all(8),
color: Colors.green,
child: Center(child: Text('Item 2')),
),
Container(
width: 160,
margin: EdgeInsets.all(8),
color: Colors.red,
child: Center(child: Text('Item 3')),
),
],
)

2. GridView Widget

GridView là widget được sử dụng để hiển thị dữ liệu dạng lưới với các cột và hàng.

GridView Widget Examples

2.1. GridView.count

GridView.count(
crossAxisCount: 2,
children: [
Container(
margin: EdgeInsets.all(8),
color: Colors.blue,
child: Center(child: Text('Item 1')),
),
Container(
margin: EdgeInsets.all(8),
color: Colors.green,
child: Center(child: Text('Item 2')),
),
Container(
margin: EdgeInsets.all(8),
color: Colors.red,
child: Center(child: Text('Item 3')),
),
Container(
margin: EdgeInsets.all(8),
color: Colors.yellow,
child: Center(child: Text('Item 4')),
),
],
)

2.2. GridView.builder

GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: items.length,
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(items[index].title),
),
);
},
)

2.3. GridView.extent

GridView.extent(
maxCrossAxisExtent: 200,
children: [
Container(
margin: EdgeInsets.all(8),
color: Colors.blue,
child: Center(child: Text('Item 1')),
),
Container(
margin: EdgeInsets.all(8),
color: Colors.green,
child: Center(child: Text('Item 2')),
),
Container(
margin: EdgeInsets.all(8),
color: Colors.red,
child: Center(child: Text('Item 3')),
),
Container(
margin: EdgeInsets.all(8),
color: Colors.yellow,
child: Center(child: Text('Item 4')),
),
],
)

3. Best Practices

3.1. ListView

  • Sử dụng ListView.builder cho danh sách dài
  • Thêm padding và spacing phù hợp
  • Xử lý scroll physics khi cần
  • Sử dụng const constructor khi có thể

3.2. GridView

  • Chọn số cột phù hợp với kích thước màn hình
  • Thêm spacing giữa các item
  • Sử dụng GridView.builder cho danh sách dài
  • Tối ưu hóa kích thước item

3.3. Performance

  • Sử dụng const constructor
  • Tránh rebuild không cần thiết
  • Sử dụng ListView.builder và GridView.builder
  • Tối ưu hóa widget tree

4. Ví dụ thực tế

4.1. Danh sách sản phẩm

ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return Card(
child: ListTile(
leading: Image.network(product.imageUrl),
title: Text(product.name),
subtitle: Text(product.price.toString()),
trailing: IconButton(
icon: Icon(Icons.shopping_cart),
onPressed: () {
// Add to cart
},
),
),
);
},
)

4.2. Lưới hình ảnh

GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
),
itemCount: images.length,
itemBuilder: (context, index) {
return Image.network(
images[index],
fit: BoxFit.cover,
);
},
)

Kết luận

ListView và GridView là những widget mạnh mẽ trong Flutter để hiển thị dữ liệu dạng danh sách và lưới. Việc hiểu rõ cách sử dụng chúng sẽ giúp bạn tạo ra giao diện người dùng hiệu quả và đẹp mắt.


Tài liệu tham khảo:

Material Design và Cupertino Widgets trong Flutter

· 4 min read

Flutter cung cấp hai bộ widget chính để tạo giao diện người dùng: Material Design cho Android và Cupertino cho iOS. Bài viết này sẽ giúp bạn hiểu rõ về hai phong cách thiết kế này và cách sử dụng chúng.

1. Material Design

Material Design là ngôn ngữ thiết kế của Google, được sử dụng chủ yếu cho các ứng dụng Android.

1.1. Các widget Material cơ bản

AppBar

AppBar(
title: const Text('Material App'),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {},
),
],
)

FloatingActionButton

FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
)

Card

Card(
child: ListTile(
leading: const Icon(Icons.person),
title: const Text('John Doe'),
subtitle: const Text('john.doe@example.com'),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () {},
),
),
)

BottomNavigationBar

BottomNavigationBar(
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: 'Business',
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
label: 'School',
),
],
)

1.2. Material Design Themes

MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.light,
textTheme: const TextTheme(
headline1: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
bodyText1: TextStyle(fontSize: 16),
),
),
home: const MyHomePage(),
)

2. Cupertino Widgets

Cupertino widgets là bộ widget theo phong cách iOS, cung cấp giao diện native cho người dùng iOS.

2.1. Các widget Cupertino cơ bản

CupertinoNavigationBar

CupertinoNavigationBar(
middle: const Text('Cupertino App'),
trailing: CupertinoButton(
child: const Icon(CupertinoIcons.add),
onPressed: () {},
),
)

CupertinoButton

CupertinoButton(
color: CupertinoColors.activeBlue,
child: const Text('Cupertino Button'),
onPressed: () {},
)

CupertinoListTile

CupertinoListTile(
leading: const Icon(CupertinoIcons.person),
title: const Text('John Doe'),
subtitle: const Text('john.doe@example.com'),
trailing: const Icon(CupertinoIcons.chevron_right),
)

CupertinoTabBar

CupertinoTabBar(
items: const [
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.settings),
label: 'Settings',
),
],
)

2.2. Cupertino Themes

CupertinoApp(
theme: const CupertinoThemeData(
primaryColor: CupertinoColors.systemBlue,
brightness: Brightness.light,
textTheme: CupertinoTextThemeData(
primaryColor: CupertinoColors.systemBlue,
),
),
home: const MyHomePage(),
)

3. So sánh Material và Cupertino

3.1. Điểm khác biệt chính

Đặc điểmMaterial DesignCupertino
Phong cáchGoogle MaterialApple iOS
Màu sắcĐa dạng, tươi sángTối giản, trung tính
AnimationPhức tạp, mượt màĐơn giản, nhanh
TypographyRobotoSan Francisco
IconographyMaterial IconsSF Symbols

3.2. Khi nào sử dụng cái nào?

Sử dụng Material Design khi:

  • Ứng dụng chủ yếu cho Android
  • Cần giao diện phong phú, nhiều animation
  • Muốn tùy chỉnh nhiều về giao diện
  • Cần các widget phức tạp

Sử dụng Cupertino khi:

  • Ứng dụng chủ yếu cho iOS
  • Cần giao diện đơn giản, tối giản
  • Muốn trải nghiệm native iOS
  • Cần hiệu năng tốt

4. Kết hợp Material và Cupertino

4.1. Platform-aware widgets

Widget build(BuildContext context) {
return Platform.isIOS
? CupertinoButton(
child: const Text('iOS Button'),
onPressed: () {},
)
: ElevatedButton(
onPressed: () {},
child: const Text('Android Button'),
);
}

4.2. Adaptive widgets

Scaffold(
appBar: AppBar(
title: const Text('Adaptive App'),
actions: [
IconButton(
icon: Icon(
Platform.isIOS
? CupertinoIcons.add
: Icons.add,
),
onPressed: () {},
),
],
),
body: Center(
child: Platform.isIOS
? const CupertinoActivityIndicator()
: const CircularProgressIndicator(),
),
)

5. Best Practices

5.1. Thiết kế nhất quán

  • Chọn một phong cách chính cho ứng dụng
  • Sử dụng các widget phù hợp với platform
  • Duy trì tính nhất quán trong toàn bộ ứng dụng

5.2. Performance

  • Sử dụng const constructor khi có thể
  • Tránh rebuild không cần thiết
  • Tối ưu hóa animation

5.3. Accessibility

  • Sử dụng semantic labels
  • Đảm bảo contrast ratio phù hợp
  • Hỗ trợ screen readers

Kết luận

Material Design và Cupertino widgets là hai công cụ mạnh mẽ trong Flutter để tạo giao diện người dùng. Việc hiểu rõ sự khác biệt và biết khi nào sử dụng cái nào sẽ giúp bạn tạo ra ứng dụng có trải nghiệm người dùng tốt nhất trên cả Android và iOS.


Tài liệu tham khảo: