Skip to main content

3 posts tagged with "Tutorial"

View All Tags

Hướng dẫn sử dụng Bloc trong ứng dụng đăng nhập

· 9 min read

Flutter Bloc Login App

Giới thiệu

Bloc (Business Logic Component) là một pattern quản lý state mạnh mẽ trong Flutter, đặc biệt phù hợp cho các ứng dụng có business logic phức tạp như authentication. Trong bài viết này, chúng ta sẽ xây dựng một hệ thống đăng nhập hoàn chỉnh sử dụng Bloc pattern.

Yêu cầu dự án

Ứng dụng đăng nhập sẽ có:

  • ✅ Màn hình đăng nhập với email và password
  • ✅ Validation form
  • ✅ Xử lý loading state
  • ✅ Hiển thị lỗi
  • ✅ Navigation sau khi đăng nhập thành công
  • ✅ Màn hình home sau khi đăng nhập

Bước 1: Setup dự án

Tạo Flutter project

flutter create bloc_login_app
cd bloc_login_app

Thêm dependencies

Thêm vào file pubspec.yaml:

dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.3
equatable: ^2.0.5
formz: ^0.6.1

Chạy lệnh:

flutter pub get

Bước 2: Tạo Models và Validators

Email Model

Tạo file lib/models/email.dart:

import 'package:formz/formz.dart';

enum EmailValidationError { invalid }

class Email extends FormzInput<String, EmailValidationError> {
const Email.pure() : super.pure('');
const Email.dirty([super.value = '']) : super.dirty();

static final _emailRegex = RegExp(
r'^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$',
);


EmailValidationError? validator(String value) {
return _emailRegex.hasMatch(value) ? null : EmailValidationError.invalid;
}
}

Password Model

Tạo file lib/models/password.dart:

import 'package:formz/formz.dart';

enum PasswordValidationError { invalid }

class Password extends FormzInput<String, PasswordValidationError> {
const Password.pure() : super.pure('');
const Password.dirty([super.value = '']) : super.dirty();


PasswordValidationError? validator(String value) {
return value.length >= 6 ? null : PasswordValidationError.invalid;
}
}

Bước 3: Tạo Login Events

Tạo file lib/bloc/login/login_event.dart:

import 'package:equatable/equatable.dart';
import '../models/email.dart';
import '../models/password.dart';

abstract class LoginEvent extends Equatable {
const LoginEvent();


List<Object> get props => [];
}

class LoginEmailChanged extends LoginEvent {
final String email;

const LoginEmailChanged(this.email);


List<Object> get props => [email];
}

class LoginPasswordChanged extends LoginEvent {
final String password;

const LoginPasswordChanged(this.password);


List<Object> get props => [password];
}

class LoginSubmitted extends LoginEvent {
const LoginSubmitted();
}

class LoginPasswordVisibilityToggled extends LoginEvent {
const LoginPasswordVisibilityToggled();
}

Bước 4: Tạo Login State

Tạo file lib/bloc/login/login_state.dart:

import 'package:equatable/equatable.dart';
import 'package:formz/formz.dart';
import '../models/email.dart';
import '../models/password.dart';

class LoginState extends Equatable {
const LoginState({
this.email = const Email.pure(),
this.password = const Password.pure(),
this.status = FormzSubmissionStatus.initial,
this.isPasswordVisible = false,
this.errorMessage,
});

final Email email;
final Password password;
final FormzSubmissionStatus status;
final bool isPasswordVisible;
final String? errorMessage;

bool get isValid => Formz.validate([email, password]);

LoginState copyWith({
Email? email,
Password? password,
FormzSubmissionStatus? status,
bool? isPasswordVisible,
String? errorMessage,
}) {
return LoginState(
email: email ?? this.email,
password: password ?? this.password,
status: status ?? this.status,
isPasswordVisible: isPasswordVisible ?? this.isPasswordVisible,
errorMessage: errorMessage ?? this.errorMessage,
);
}


List<Object?> get props => [
email,
password,
status,
isPasswordVisible,
errorMessage,
];
}

Bước 5: Tạo Login Bloc

Tạo file lib/bloc/login/login_bloc.dart:

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:formz/formz.dart';
import 'login_event.dart';
import 'login_state.dart';
import '../models/email.dart';
import '../models/password.dart';

class LoginBloc extends Bloc<LoginEvent, LoginState> {
LoginBloc() : super(const LoginState()) {
on<LoginEmailChanged>(_onEmailChanged);
on<LoginPasswordChanged>(_onPasswordChanged);
on<LoginSubmitted>(_onSubmitted);
on<LoginPasswordVisibilityToggled>(_onPasswordVisibilityToggled);
}

void _onEmailChanged(
LoginEmailChanged event,
Emitter<LoginState> emit,
) {
final email = Email.dirty(event.email);
emit(
state.copyWith(
email: email,
status: Formz.validate([email, state.password])
? FormzSubmissionStatus.initial
: FormzSubmissionStatus.initial,
),
);
}

void _onPasswordChanged(
LoginPasswordChanged event,
Emitter<LoginState> emit,
) {
final password = Password.dirty(event.password);
emit(
state.copyWith(
password: password,
status: Formz.validate([state.email, password])
? FormzSubmissionStatus.initial
: FormzSubmissionStatus.initial,
),
);
}

void _onPasswordVisibilityToggled(
LoginPasswordVisibilityToggled event,
Emitter<LoginState> emit,
) {
emit(state.copyWith(isPasswordVisible: !state.isPasswordVisible));
}

Future<void> _onSubmitted(
LoginSubmitted event,
Emitter<LoginState> emit,
) async {
if (state.isValid) {
emit(state.copyWith(status: FormzSubmissionStatus.inProgress));
try {
// Simulate API call
await Future.delayed(const Duration(seconds: 2));

// Mock authentication logic
if (state.email.value == 'user@example.com' &&
state.password.value == 'password123') {
emit(state.copyWith(status: FormzSubmissionStatus.success));
} else {
emit(
state.copyWith(
status: FormzSubmissionStatus.failure,
errorMessage: 'Email hoặc mật khẩu không đúng',
),
);
}
} catch (error) {
emit(
state.copyWith(
status: FormzSubmissionStatus.failure,
errorMessage: 'Đã xảy ra lỗi. Vui lòng thử lại.',
),
);
}
}
}
}

Bước 6: Tạo Auth Repository (Optional)

Tạo file lib/repositories/auth_repository.dart:

class AuthRepository {
Future<bool> login(String email, String password) async {
// Simulate API call
await Future.delayed(const Duration(seconds: 2));

// Mock authentication
if (email == 'user@example.com' && password == 'password123') {
return true;
}
return false;
}

Future<void> logout() async {
// Clear user session
await Future.delayed(const Duration(milliseconds: 500));
}
}

Bước 7: Tạo Login Screen

Tạo file lib/screens/login_screen.dart:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/login/login_bloc.dart';
import '../bloc/login/login_event.dart';
import '../bloc/login/login_state.dart';
import 'home_screen.dart';

class LoginScreen extends StatelessWidget {
const LoginScreen({Key? key}) : super(key: key);


Widget build(BuildContext context) {
return Scaffold(
body: BlocProvider(
create: (context) => LoginBloc(),
child: const LoginForm(),
),
);
}
}

class LoginForm extends StatelessWidget {
const LoginForm({Key? key}) : super(key: key);


Widget build(BuildContext context) {
return BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
if (state.status.isSuccess) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const HomeScreen()),
);
} else if (state.status.isFailure) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text(state.errorMessage ?? 'Đăng nhập thất bại'),
backgroundColor: Colors.red,
),
);
}
},
child: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.lock_outline,
size: 80,
color: Colors.blue,
),
const SizedBox(height: 32),
const Text(
'Đăng Nhập',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 48),
const _EmailInput(),
const SizedBox(height: 16),
const _PasswordInput(),
const SizedBox(height: 24),
const _LoginButton(),
const SizedBox(height: 16),
const _DemoCredentials(),
],
),
),
),
),
);
}
}

class _EmailInput extends StatelessWidget {
const _EmailInput();


Widget build(BuildContext context) {
return BlocBuilder<LoginBloc, LoginState>(
buildWhen: (previous, current) => previous.email != current.email,
builder: (context, state) {
return TextField(
key: const Key('loginForm_emailInput_textField'),
onChanged: (email) =>
context.read<LoginBloc>().add(LoginEmailChanged(email)),
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'Email',
prefixIcon: const Icon(Icons.email),
errorText: state.email.displayError != null
? 'Email không hợp lệ'
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
},
);
}
}

class _PasswordInput extends StatelessWidget {
const _PasswordInput();


Widget build(BuildContext context) {
return BlocBuilder<LoginBloc, LoginState>(
buildWhen: (previous, current) =>
previous.password != current.password ||
previous.isPasswordVisible != current.isPasswordVisible,
builder: (context, state) {
return TextField(
key: const Key('loginForm_passwordInput_textField'),
onChanged: (password) =>
context.read<LoginBloc>().add(LoginPasswordChanged(password)),
obscureText: !state.isPasswordVisible,
decoration: InputDecoration(
labelText: 'Mật khẩu',
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
state.isPasswordVisible
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () => context
.read<LoginBloc>()
.add(const LoginPasswordVisibilityToggled()),
),
errorText: state.password.displayError != null
? 'Mật khẩu phải có ít nhất 6 ký tự'
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
},
);
}
}

class _LoginButton extends StatelessWidget {
const _LoginButton();


Widget build(BuildContext context) {
return BlocBuilder<LoginBloc, LoginState>(
builder: (context, state) {
return SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
key: const Key('loginForm_continue_raisedButton'),
onPressed: state.isValid && !state.status.isInProgress
? () => context.read<LoginBloc>().add(const LoginSubmitted())
: null,
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: state.status.isInProgress
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text(
'Đăng Nhập',
style: TextStyle(fontSize: 16),
),
),
);
},
);
}
}

class _DemoCredentials extends StatelessWidget {
const _DemoCredentials();


Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
const Text(
'Thông tin đăng nhập demo:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text('Email: user@example.com'),
const Text('Password: password123'),
],
),
);
}
}

Bước 8: Tạo Home Screen

Tạo file lib/screens/home_screen.dart:

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Trang Chủ'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
Navigator.of(context).pop();
},
tooltip: 'Đăng xuất',
),
],
),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.check_circle,
size: 80,
color: Colors.green,
),
SizedBox(height: 24),
Text(
'Đăng nhập thành công!',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 16),
Text(
'Chào mừng bạn đến với ứng dụng',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
],
),
),
);
}
}

Bước 9: Setup main.dart

Cập nhật file lib/main.dart:

import 'package:flutter/material.dart';
import 'screens/login_screen.dart';

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

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);


Widget build(BuildContext context) {
return MaterialApp(
title: 'Bloc Login App',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const LoginScreen(),
debugShowCheckedModeBanner: false,
);
}
}

Giải thích các khái niệm Bloc

1. Events

Events đại diện cho các hành động từ UI (user input, button clicks, etc.). Mỗi event là một class extends Equatable.

2. States

States đại diện cho trạng thái hiện tại của ứng dụng. State class cũng extends Equatable để so sánh.

3. Bloc

Bloc nhận Events và emit States. Nó chứa business logic và xử lý các side effects.

4. BlocBuilder

BlocBuilder rebuild UI khi state thay đổi. Sử dụng buildWhen để kiểm soát khi nào rebuild.

5. BlocListener

BlocListener lắng nghe state changes và thực hiện side effects (navigation, show snackbar, etc.) mà không rebuild UI.

Best Practices

1. Separation of Concerns

  • Events: User actions
  • States: UI state
  • Bloc: Business logic

2. Immutable States

Luôn tạo state mới thay vì modify state hiện tại:

// ✅ Good
emit(state.copyWith(email: newEmail));

// ❌ Bad
state.email = newEmail;
emit(state);

3. Use Equatable

Sử dụng Equatable để so sánh states và events hiệu quả.

4. Error Handling

Luôn xử lý errors và emit appropriate states:

try {
// API call
} catch (error) {
emit(state.copyWith(
status: FormzSubmissionStatus.failure,
errorMessage: error.toString(),
));
}

5. Testing

Bloc dễ test vì logic tách biệt khỏi UI:

test('emits [LoginState] when email changes', () {
final bloc = LoginBloc();
bloc.add(const LoginEmailChanged('test@example.com'));
expect(bloc.state.email.value, 'test@example.com');
});

Mở rộng ứng dụng

Thêm Remember Me

class LoginRememberMeToggled extends LoginEvent {
const LoginRememberMeToggled();
}

// In state
final bool rememberMe;

// In bloc
void _onRememberMeToggled(...) {
emit(state.copyWith(rememberMe: !state.rememberMe));
}

Thêm Forgot Password

Tạo ForgotPasswordBloc tương tự LoginBloc.

Tích hợp với API thật

Thay thế mock logic bằng API calls:

Future<void> _onSubmitted(...) async {
if (state.isValid) {
emit(state.copyWith(status: FormzSubmissionStatus.inProgress));
try {
final success = await authRepository.login(
state.email.value,
state.password.value,
);
if (success) {
emit(state.copyWith(status: FormzSubmissionStatus.success));
} else {
emit(state.copyWith(
status: FormzSubmissionStatus.failure,
errorMessage: 'Đăng nhập thất bại',
));
}
} catch (error) {
emit(state.copyWith(
status: FormzSubmissionStatus.failure,
errorMessage: error.toString(),
));
}
}
}

Kết luận

Trong bài viết này, chúng ta đã xây dựng một hệ thống đăng nhập hoàn chỉnh sử dụng Bloc pattern. Các điểm chính:

  1. ✅ Tạo Events và States
  2. ✅ Implement LoginBloc với business logic
  3. ✅ Sử dụng BlocBuilder và BlocListener
  4. ✅ Form validation với Formz
  5. ✅ Xử lý loading và error states

Bloc pattern cung cấp một architecture rõ ràng và dễ test, đặc biệt phù hợp cho các ứng dụng có business logic phức tạp. Hãy tiếp tục khám phá và mở rộng ứng dụng này!

Happy coding với Flutter và Bloc!

GetX: Quản lý state + Routing + Dependency Injection

· 10 min read

Flutter GetX Complete Guide

Giới thiệu

GetX là một giải pháp all-in-one mạnh mẽ cho Flutter, cung cấp không chỉ state management mà còn routing, dependency injection, và nhiều tính năng khác. Trong bài viết này, chúng ta sẽ khám phá toàn bộ sức mạnh của GetX.

Tổng quan về GetX

GetX bao gồm 3 phần chính:

  1. State Management: Quản lý state reactive
  2. Route Management: Navigation và routing
  3. Dependency Management: Dependency injection

Ưu điểm

  • All-in-one: Một package cho nhiều tính năng
  • Hiệu suất cao: Tối ưu rebuild
  • Syntax đơn giản: Code ngắn gọn
  • Không cần BuildContext: Truy cập từ bất kỳ đâu
  • Tích hợp nhiều tính năng: Dialogs, snackbars, bottom sheets

Nhược điểm

  • ⚠️ Không được Flutter team chính thức khuyến nghị
  • ⚠️ Tight coupling giữa các tính năng
  • ⚠️ Có thể khó maintain với ứng dụng lớn

Phần 1: State Management với GetX

Setup

Thêm vào pubspec.yaml:

dependencies:
get: ^4.6.6

Reactive State Management

1. Sử dụng .obs (Observable)

import 'package:get/get.dart';

class CounterController extends GetxController {
var count = 0.obs; // Observable variable

void increment() => count++;
void decrement() => count--;
}

2. Sử dụng trong Widget

class CounterScreen extends StatelessWidget {
final CounterController controller = Get.put(CounterController());


Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Obx(() => Text('Count: ${controller.count}')),
),
floatingActionButton: FloatingActionButton(
onPressed: () => controller.increment(),
child: Icon(Icons.add),
),
);
}
}

State Management Patterns

Pattern 1: Simple State với .obs

class UserController extends GetxController {
var name = ''.obs;
var age = 0.obs;
var isLoggedIn = false.obs;

void login(String userName, int userAge) {
name.value = userName;
age.value = userAge;
isLoggedIn.value = true;
}

void logout() {
name.value = '';
age.value = 0;
isLoggedIn.value = false;
}
}

Pattern 2: Custom Class với Rx

class User {
final String name;
final int age;

User({required this.name, required this.age});
}

class UserController extends GetxController {
var user = User(name: '', age: 0).obs;

void updateUser(String name, int age) {
user.value = User(name: name, age: age);
}
}

Pattern 3: GetxController với Lifecycle

class ProductController extends GetxController {
var products = <Product>[].obs;
var isLoading = false.obs;


void onInit() {
super.onInit();
fetchProducts();
}


void onReady() {
super.onReady();
// Called after widget is rendered
}


void onClose() {
super.onClose();
// Clean up resources
}

Future<void> fetchProducts() async {
isLoading.value = true;
try {
// API call
await Future.delayed(Duration(seconds: 2));
products.value = [/* products */];
} finally {
isLoading.value = false;
}
}
}

Workers: Lắng nghe thay đổi

class CounterController extends GetxController {
var count = 0.obs;


void onInit() {
super.onInit();

// Lắng nghe mọi thay đổi
ever(count, (value) {
print('Count changed to: $value');
});

// Lắng nghe lần đầu tiên
once(count, (value) {
print('Count changed for the first time: $value');
});

// Debounce: Chờ 1 giây sau khi thay đổi cuối cùng
debounce(count, (value) {
print('Count debounced: $value');
}, time: Duration(seconds: 1));

// Interval: Chạy mỗi 1 giây nếu có thay đổi
interval(count, (value) {
print('Count interval: $value');
}, time: Duration(seconds: 1));
}

void increment() => count++;
}

Phần 2: Route Management với GetX

Setup

Thay vì MaterialApp, sử dụng GetMaterialApp:

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

class MyApp extends StatelessWidget {

Widget build(BuildContext context) {
return GetMaterialApp(
title: 'GetX App',
initialRoute: '/',
getPages: [
GetPage(name: '/', page: () => HomeScreen()),
GetPage(name: '/details', page: () => DetailsScreen()),
GetPage(
name: '/profile/:userId',
page: () => ProfileScreen(),
),
],
);
}
}

1. Navigate to Screen

// Navigate to named route
Get.toNamed('/details');

// Navigate with arguments
Get.toNamed('/details', arguments: {'id': 123});

// Navigate and remove current screen
Get.offNamed('/details');

// Navigate and remove all previous screens
Get.offAllNamed('/details');

// Navigate and get result back
final result = await Get.toNamed('/details');

2. Navigate with Parameters

// Navigate with path parameters
Get.toNamed('/profile/123'); // userId = 123

// In ProfileScreen
class ProfileScreen extends StatelessWidget {

Widget build(BuildContext context) {
final userId = Get.parameters['userId'];
return Scaffold(
body: Text('User ID: $userId'),
);
}
}

3. Navigate with Arguments

// Pass arguments
Get.toNamed('/details', arguments: {
'title': 'Product Details',
'price': 99.99,
});

// Receive arguments
class DetailsScreen extends StatelessWidget {

Widget build(BuildContext context) {
final args = Get.arguments;
final title = args['title'];
final price = args['price'];

return Scaffold(
appBar: AppBar(title: Text(title)),
body: Text('Price: \$$price'),
);
}
}

4. Go Back

// Go back
Get.back();

// Go back with result
Get.back(result: 'some result');

// Go back to specific route
Get.until((route) => route.settings.name == '/home');

Advanced Routing

Named Routes với Bindings

class DetailsBinding extends Bindings {

void dependencies() {
Get.lazyPut(() => DetailsController());
}
}

GetPage(
name: '/details',
page: () => DetailsScreen(),
binding: DetailsBinding(),
),

Middleware (Route Guards)

class AuthMiddleware extends GetMiddleware {

RouteSettings? redirect(String? route) {
// Check if user is authenticated
if (!AuthService.isAuthenticated) {
return RouteSettings(name: '/login');
}
return null;
}
}

GetPage(
name: '/profile',
page: () => ProfileScreen(),
middlewares: [AuthMiddleware()],
),

Transition Animations

Get.to(
DetailsScreen(),
transition: Transition.fade,
duration: Duration(milliseconds: 300),
);

// Available transitions:
// - Transition.fade
// - Transition.rightToLeft
// - Transition.leftToRight
// - Transition.upToDown
// - Transition.downToUp
// - Transition.zoom
// - Transition.cupertino

Phần 3: Dependency Injection với GetX

Các phương thức Dependency Injection

1. Get.put() - Immediate initialization

// Tạo instance ngay lập tức
final controller = Get.put(CounterController());

// Với tag để phân biệt
final controller1 = Get.put(CounterController(), tag: 'counter1');
final controller2 = Get.put(CounterController(), tag: 'counter2');

2. Get.lazyPut() - Lazy initialization

// Tạo instance khi cần
Get.lazyPut(() => CounterController());

// Sử dụng
final controller = Get.find<CounterController>();

3. Get.putAsync() - Async initialization

// Tạo instance async
Get.putAsync(() async {
final prefs = await SharedPreferences.getInstance();
return SettingsController(prefs);
});

4. Get.create() - Create new instance mỗi lần

// Tạo instance mới mỗi lần gọi
Get.create(() => CounterController());

Dependency Management Patterns

Pattern 1: Sử dụng trong Controller

class ProductController extends GetxController {
final ProductService productService;

ProductController({required this.productService});

var products = <Product>[].obs;

Future<void> fetchProducts() async {
products.value = await productService.getProducts();
}
}

// Setup
Get.put(ProductService());
Get.put(ProductController(productService: Get.find()));

Pattern 2: Sử dụng Bindings

class ProductBinding extends Bindings {

void dependencies() {
Get.lazyPut(() => ProductService());
Get.lazyPut(() => ProductController(productService: Get.find()));
}
}

// Sử dụng
GetPage(
name: '/products',
page: () => ProductsScreen(),
binding: ProductBinding(),
),

Pattern 3: Global Bindings

class AppBindings extends Bindings {

void dependencies() {
// Global dependencies
Get.put(AuthService(), permanent: true);
Get.put(UserService(), permanent: true);
}
}

// Trong main.dart
GetMaterialApp(
initialBinding: AppBindings(),
// ...
)

Dependency Lifecycle

// Permanent: Không bị xóa khi không dùng
Get.put(Service(), permanent: true);

// Remove dependency
Get.delete<CounterController>();

// Remove all dependencies
Get.reset();

// Check if exists
if (Get.isRegistered<CounterController>()) {
final controller = Get.find<CounterController>();
}

Ví dụ hoàn chỉnh: Todo App với GetX

1. Model

class Todo {
final String id;
final String title;
final bool isCompleted;

Todo({
required this.id,
required this.title,
this.isCompleted = false,
});
}

2. Controller

class TodoController extends GetxController {
var todos = <Todo>[].obs;
var filter = 'all'.obs; // all, active, completed

List<Todo> get filteredTodos {
switch (filter.value) {
case 'active':
return todos.where((todo) => !todo.isCompleted).toList();
case 'completed':
return todos.where((todo) => todo.isCompleted).toList();
default:
return todos;
}
}

void addTodo(String title) {
todos.add(Todo(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title,
));
}

void toggleTodo(String id) {
final index = todos.indexWhere((todo) => todo.id == id);
if (index != -1) {
todos[index] = Todo(
id: todos[index].id,
title: todos[index].title,
isCompleted: !todos[index].isCompleted,
);
}
}

void deleteTodo(String id) {
todos.removeWhere((todo) => todo.id == id);
}

void setFilter(String newFilter) {
filter.value = newFilter;
}
}

3. Screen

class TodoScreen extends StatelessWidget {
final TodoController controller = Get.put(TodoController());
final textController = TextEditingController();


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Todo App')),
body: Column(
children: [
// Filter buttons
Obx(() => Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildFilterButton('all', controller.filter.value == 'all'),
_buildFilterButton('active', controller.filter.value == 'active'),
_buildFilterButton('completed', controller.filter.value == 'completed'),
],
)),

// Todo list
Expanded(
child: Obx(() => ListView.builder(
itemCount: controller.filteredTodos.length,
itemBuilder: (context, index) {
final todo = controller.filteredTodos[index];
return ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) => controller.toggleTodo(todo.id),
),
title: Text(todo.title),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => controller.deleteTodo(todo.id),
),
);
},
)),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddDialog(),
child: Icon(Icons.add),
),
);
}

void _showAddDialog() {
Get.dialog(
AlertDialog(
title: Text('Thêm Todo'),
content: TextField(
controller: textController,
autofocus: true,
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('Hủy'),
),
TextButton(
onPressed: () {
if (textController.text.isNotEmpty) {
controller.addTodo(textController.text);
textController.clear();
Get.back();
}
},
child: Text('Thêm'),
),
],
),
);
}

Widget _buildFilterButton(String label, bool isSelected) {
return ElevatedButton(
onPressed: () => controller.setFilter(label),
style: ElevatedButton.styleFrom(
backgroundColor: isSelected ? Colors.blue : Colors.grey,
),
child: Text(label),
);
}
}

GetX Utilities

Snackbars

Get.snackbar(
'Title',
'Message',
snackPosition: SnackPosition.BOTTOM,
duration: Duration(seconds: 3),
backgroundColor: Colors.blue,
colorText: Colors.white,
);

Dialogs

Get.dialog(
AlertDialog(
title: Text('Title'),
content: Text('Content'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('OK'),
),
],
),
);

Bottom Sheets

Get.bottomSheet(
Container(
height: 200,
child: Column(
children: [
ListTile(
leading: Icon(Icons.share),
title: Text('Share'),
onTap: () => Get.back(),
),
],
),
),
);

Internationalization

// translations.dart
class Messages extends Translations {

Map<String, Map<String, String>> get keys => {
'en_US': {
'hello': 'Hello',
},
'vi_VN': {
'hello': 'Xin chào',
},
};
}

// Usage
GetMaterialApp(
translations: Messages(),
locale: Locale('vi', 'VN'),
// ...
)

Text('hello'.tr) // 'Xin chào'

Best Practices

1. Tổ chức code

lib/
controllers/
todo_controller.dart
user_controller.dart
models/
todo.dart
user.dart
views/
todo_screen.dart
user_screen.dart
bindings/
todo_binding.dart
routes/
app_routes.dart

2. Sử dụng Bindings

Luôn sử dụng Bindings để quản lý dependencies:

class TodoBinding extends Bindings {

void dependencies() {
Get.lazyPut(() => TodoController());
}
}

3. Tách biệt logic và UI

  • Controllers: Business logic
  • Views: UI only
  • Models: Data structures

4. Sử dụng Workers hợp lý


void onInit() {
super.onInit();
// Chỉ lắng nghe những gì cần thiết
ever(count, (value) {
// Important logic
});
}

5. Clean up resources


void onClose() {
// Dispose controllers, streams, etc.
super.onClose();
}

Kết luận

GetX là một giải pháp mạnh mẽ và toàn diện cho Flutter development. Các điểm chính:

  1. State Management: Reactive và hiệu quả
  2. Routing: Đơn giản và mạnh mẽ
  3. Dependency Injection: Dễ sử dụng
  4. Utilities: Snackbars, dialogs, bottom sheets
  5. Internationalization: Hỗ trợ đa ngôn ngữ

GetX phù hợp cho:

  • Ứng dụng nhỏ đến trung bình
  • Muốn giải pháp all-in-one
  • Ưu tiên code ngắn gọn
  • Cần routing đơn giản

GetX cung cấp một cách tiếp cận đơn giản và mạnh mẽ để xây dựng ứng dụng Flutter. Hãy thử nghiệm và khám phá sức mạnh của nó!

Hướng dẫn sử dụng Provider để xây dựng app Todo

· 9 min read

Flutter Provider Todo App

Giới thiệu

Trong bài viết này, chúng ta sẽ xây dựng một ứng dụng Todo hoàn chỉnh sử dụng Provider để quản lý state. Đây là một dự án thực tế giúp bạn hiểu cách sử dụng Provider trong Flutter một cách hiệu quả.

Yêu cầu dự án

Ứng dụng Todo của chúng ta sẽ có các tính năng:

  • ✅ Thêm task mới
  • ✅ Xóa task
  • ✅ Đánh dấu task hoàn thành
  • ✅ Lọc task (Tất cả, Đang làm, Hoàn thành)
  • ✅ Lưu trữ dữ liệu local

Bước 1: Setup dự án

Tạo Flutter project

flutter create todo_provider_app
cd todo_provider_app

Thêm dependencies

Thêm vào file pubspec.yaml:

dependencies:
flutter:
sdk: flutter
provider: ^6.1.1
shared_preferences: ^2.2.2
uuid: ^4.2.1

Chạy lệnh:

flutter pub get

Bước 2: Tạo Model

Tạo file lib/models/todo.dart:

class Todo {
final String id;
final String title;
final bool isCompleted;
final DateTime createdAt;

Todo({
required this.id,
required this.title,
this.isCompleted = false,
required this.createdAt,
});

Todo copyWith({
String? id,
String? title,
bool? isCompleted,
DateTime? createdAt,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
isCompleted: isCompleted ?? this.isCompleted,
createdAt: createdAt ?? this.createdAt,
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'isCompleted': isCompleted,
'createdAt': createdAt.toIso8601String(),
};
}

factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
id: json['id'],
title: json['title'],
isCompleted: json['isCompleted'] ?? false,
createdAt: DateTime.parse(json['createdAt']),
);
}
}

Bước 3: Tạo Provider

Tạo file lib/providers/todo_provider.dart:

import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import '../models/todo.dart';
import 'package:uuid/uuid.dart';

enum FilterType { all, active, completed }

class TodoProvider with ChangeNotifier {
List<Todo> _todos = [];
FilterType _filter = FilterType.all;

List<Todo> get todos {
switch (_filter) {
case FilterType.active:
return _todos.where((todo) => !todo.isCompleted).toList();
case FilterType.completed:
return _todos.where((todo) => todo.isCompleted).toList();
default:
return _todos;
}
}

FilterType get filter => _filter;
int get activeTodosCount => _todos.where((todo) => !todo.isCompleted).length;
int get completedTodosCount => _todos.where((todo) => todo.isCompleted).length;

TodoProvider() {
_loadTodos();
}

// Load todos from SharedPreferences
Future<void> _loadTodos() async {
try {
final prefs = await SharedPreferences.getInstance();
final todosJson = prefs.getStringList('todos') ?? [];
_todos = todosJson
.map((json) => Todo.fromJson(jsonDecode(json)))
.toList();
notifyListeners();
} catch (e) {
debugPrint('Error loading todos: $e');
}
}

// Save todos to SharedPreferences
Future<void> _saveTodos() async {
try {
final prefs = await SharedPreferences.getInstance();
final todosJson = _todos
.map((todo) => jsonEncode(todo.toJson()))
.toList();
await prefs.setStringList('todos', todosJson);
} catch (e) {
debugPrint('Error saving todos: $e');
}
}

// Add new todo
Future<void> addTodo(String title) async {
if (title.trim().isEmpty) return;

final newTodo = Todo(
id: const Uuid().v4(),
title: title.trim(),
createdAt: DateTime.now(),
);

_todos.add(newTodo);
notifyListeners();
await _saveTodos();
}

// Toggle todo completion
Future<void> toggleTodo(String id) async {
final index = _todos.indexWhere((todo) => todo.id == id);
if (index != -1) {
_todos[index] = _todos[index].copyWith(
isCompleted: !_todos[index].isCompleted,
);
notifyListeners();
await _saveTodos();
}
}

// Delete todo
Future<void> deleteTodo(String id) async {
_todos.removeWhere((todo) => todo.id == id);
notifyListeners();
await _saveTodos();
}

// Clear completed todos
Future<void> clearCompleted() async {
_todos.removeWhere((todo) => todo.isCompleted);
notifyListeners();
await _saveTodos();
}

// Set filter
void setFilter(FilterType filter) {
_filter = filter;
notifyListeners();
}
}

Bước 4: Tạo UI Components

Todo Item Widget

Tạo file lib/widgets/todo_item.dart:

import 'package:flutter/material.dart';
import '../models/todo.dart';

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);


Widget build(BuildContext context) {
return Dismissible(
key: Key(todo.id),
direction: DismissDirection.endToStart,
onDismissed: (_) => onDelete(),
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
color: Colors.red,
child: const Icon(Icons.delete, color: Colors.white),
),
child: ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) => onToggle(),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
color: todo.isCompleted
? Colors.grey
: Colors.black,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: onDelete,
color: Colors.red,
),
),
);
}
}

Add Todo Dialog

Tạo file lib/widgets/add_todo_dialog.dart:

import 'package:flutter/material.dart';

class AddTodoDialog extends StatefulWidget {
const AddTodoDialog({Key? key}) : super(key: key);


State<AddTodoDialog> createState() => _AddTodoDialogState();
}

class _AddTodoDialogState extends State<AddTodoDialog> {
final _controller = TextEditingController();


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


Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Thêm Task Mới'),
content: TextField(
controller: _controller,
autofocus: true,
decoration: const InputDecoration(
hintText: 'Nhập task của bạn...',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _submit(),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Hủy'),
),
ElevatedButton(
onPressed: _submit,
child: const Text('Thêm'),
),
],
);
}

void _submit() {
if (_controller.text.trim().isNotEmpty) {
Navigator.of(context).pop(_controller.text.trim());
}
}
}

Bước 5: Tạo Main Screen

Tạo file lib/screens/home_screen.dart:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/todo_provider.dart';
import '../widgets/todo_item.dart';
import '../widgets/add_todo_dialog.dart';

class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Todo App'),
actions: [
Consumer<TodoProvider>(
builder: (context, provider, _) {
if (provider.completedTodosCount > 0) {
return IconButton(
icon: const Icon(Icons.delete_sweep),
tooltip: 'Xóa task đã hoàn thành',
onPressed: () async {
await provider.clearCompleted();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đã xóa các task hoàn thành'),
),
);
}
},
);
}
return const SizedBox.shrink();
},
),
],
),
body: Column(
children: [
// Filter buttons
Consumer<TodoProvider>(
builder: (context, provider, _) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildFilterButton(
context,
'Tất cả',
FilterType.all,
provider.filter == FilterType.all,
() => provider.setFilter(FilterType.all),
),
_buildFilterButton(
context,
'Đang làm',
FilterType.active,
provider.filter == FilterType.active,
() => provider.setFilter(FilterType.active),
),
_buildFilterButton(
context,
'Hoàn thành',
FilterType.completed,
provider.filter == FilterType.completed,
() => provider.setFilter(FilterType.completed),
),
],
),
);
},
),
// Stats
Consumer<TodoProvider>(
builder: (context, provider, _) {
return Container(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatCard(
context,
'Tổng',
provider.todos.length.toString(),
Colors.blue,
),
_buildStatCard(
context,
'Đang làm',
provider.activeTodosCount.toString(),
Colors.orange,
),
_buildStatCard(
context,
'Hoàn thành',
provider.completedTodosCount.toString(),
Colors.green,
),
],
),
);
},
),
// Todo list
Expanded(
child: Consumer<TodoProvider>(
builder: (context, provider, _) {
if (provider.todos.isEmpty) {
return const Center(
child: Text(
'Chưa có task nào.\nThêm task mới để bắt đầu!',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
color: Colors.grey,
),
),
);
}

return ListView.builder(
itemCount: provider.todos.length,
itemBuilder: (context, index) {
final todo = provider.todos[index];
return TodoItem(
todo: todo,
onToggle: () => provider.toggleTodo(todo.id),
onDelete: () => provider.deleteTodo(todo.id),
);
},
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final result = await showDialog<String>(
context: context,
builder: (_) => const AddTodoDialog(),
);

if (result != null && context.mounted) {
await context.read<TodoProvider>().addTodo(result);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đã thêm task mới'),
duration: Duration(seconds: 1),
),
);
}
},
child: const Icon(Icons.add),
),
);
}

Widget _buildFilterButton(
BuildContext context,
String label,
FilterType type,
bool isSelected,
VoidCallback onTap,
) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.grey[200],
borderRadius: BorderRadius.circular(20),
),
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : Colors.black,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
),
);
}

Widget _buildStatCard(
BuildContext context,
String label,
String value,
Color color,
) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
children: [
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
);
}
}

Bước 6: Setup Provider trong main.dart

Cập nhật file lib/main.dart:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/todo_provider.dart';
import 'screens/home_screen.dart';

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

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);


Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TodoProvider(),
child: MaterialApp(
title: 'Todo App',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const HomeScreen(),
debugShowCheckedModeBanner: false,
),
);
}
}

Bước 7: Chạy ứng dụng

flutter run

Giải thích các khái niệm quan trọng

1. ChangeNotifier

TodoProvider extends ChangeNotifier, cho phép nó thông báo cho các listeners khi state thay đổi.

2. notifyListeners()

Mỗi khi state thay đổi, gọi notifyListeners() để UI tự động rebuild.

3. Consumer Widget

Consumer<TodoProvider> chỉ rebuild phần UI cần thiết khi state thay đổi, tối ưu hiệu suất.

4. context.read() vs context.watch()

  • context.read<TodoProvider>(): Dùng khi chỉ cần gọi method, không cần rebuild
  • context.watch<TodoProvider>(): Dùng khi cần rebuild khi state thay đổi

Best Practices

1. Tách biệt logic và UI

  • Provider chứa business logic
  • Widget chỉ hiển thị UI

2. Sử dụng Consumer đúng cách

  • Chỉ wrap phần UI cần rebuild
  • Tránh wrap toàn bộ widget tree

3. Xử lý async operations

  • Sử dụng Future cho async operations
  • Hiển thị loading state khi cần

4. Error handling

  • Luôn xử lý lỗi trong async operations
  • Hiển thị thông báo lỗi cho user

Mở rộng ứng dụng

Thêm tính năng chỉnh sửa task

Future<void> updateTodo(String id, String newTitle) async {
final index = _todos.indexWhere((todo) => todo.id == id);
if (index != -1) {
_todos[index] = _todos[index].copyWith(title: newTitle);
notifyListeners();
await _saveTodos();
}
}

Thêm priority cho task

enum Priority { low, medium, high }

class Todo {
// ... existing code
final Priority priority;

// ... rest of code
}

Thêm due date

class Todo {
// ... existing code
final DateTime? dueDate;

// ... rest of code
}

Kết luận

Trong bài viết này, chúng ta đã xây dựng một ứng dụng Todo hoàn chỉnh sử dụng Provider. Các điểm chính:

  1. ✅ Tạo model cho Todo
  2. ✅ Sử dụng Provider để quản lý state
  3. ✅ Tích hợp SharedPreferences để lưu trữ
  4. ✅ Xây dựng UI với Consumer
  5. ✅ Xử lý các thao tác CRUD

Provider là một giải pháp tuyệt vời cho quản lý state trong Flutter, đặc biệt phù hợp với các ứng dụng nhỏ đến trung bình. Hãy thử nghiệm và mở rộng ứng dụng này để học thêm về Provider!

Happy coding với Flutter và Provider!