Skip to main content

2 posts tagged with "Riverpod"

View All Tags

State Management Nâng Cao Trong Flutter

· 3 min read

Provider

Provider là giải pháp quản lý state đơn giản và mạnh mẽ được Flutter team khuyên dùng.

Cài đặt Provider

dependencies:
provider: ^6.0.0

Ví dụ Cơ bản với Provider

// Model
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;

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

// Widget sử dụng Provider
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => Counter(),
child: Consumer<Counter>(
builder: (context, counter, child) => Text('${counter.count}'),
),
);
}
}

Bloc (Business Logic Component)

Bloc là pattern giúp tách biệt business logic khỏi presentation layer.

Cài đặt Bloc

dependencies:
flutter_bloc: ^8.0.0

Ví dụ với Bloc

// Events
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}

// State
class CounterState {
final int count;
CounterState(this.count);
}

// Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0)) {
on<IncrementEvent>((event, emit) {
emit(CounterState(state.count + 1));
});
}
}

GetX

GetX là giải pháp all-in-one cho state management, routing và dependency injection.

Cài đặt GetX

dependencies:
get: ^4.6.0

Ví dụ với GetX

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

void increment() => count++;
}

// Widget
class CounterWidget extends StatelessWidget {
final controller = Get.put(CounterController());

@override
Widget build(BuildContext context) {
return Obx(() => Text('${controller.count}'));
}
}

Riverpod

Riverpod là phiên bản cải tiến của Provider, khắc phục một số hạn chế.

Cài đặt Riverpod

dependencies:
flutter_riverpod: ^2.0.0

Ví dụ với Riverpod

// Provider definition
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter());

class Counter extends StateNotifier<int> {
Counter() : super(0);
void increment() => state++;
}

// Widget
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}

So Sánh Các Giải Pháp

Provider

  • Ưu điểm: Đơn giản, dễ học, được Flutter team khuyên dùng
  • Nhược điểm: Có thể phức tạp khi ứng dụng lớn

Bloc

  • Ưu điểm: Mạnh mẽ, có thể mở rộng, dễ test
  • Nhược điểm: Có learning curve cao, nhiều boilerplate code

GetX

  • Ưu điểm: All-in-one solution, ít code
  • Nhược điểm: Có thể làm code khó maintain nếu không cẩn thận

Riverpod

  • Ưu điểm: Type-safe, compile-time safety
  • Nhược điểm: Khái niệm mới cần thời gian làm quen

Khi Nào Sử Dụng Gì?

  • Provider: Cho dự án nhỏ và vừa, team mới học Flutter
  • Bloc: Cho dự án lớn, cần tách biệt business logic rõ ràng
  • GetX: Cho dự án cần phát triển nhanh, ít người
  • Riverpod: Cho dự án cần type-safety cao, muốn tránh runtime errors

Tài Liệu Tham Khảo

Hướng Dẫn Sử Dụng Riverpod/Provider Cho Quản Lý State Khi Gọi API

· 10 min read

Quản lý state khi gọi API là một thách thức trong Flutter. Bài viết này sẽ hướng dẫn bạn cách sử dụng Riverpod hoặc Provider để quản lý state một cách hiệu quả khi gọi API.


1️⃣ Tại Sao Cần State Management Cho API?

1.1 Vấn Đề Khi Không Dùng State Management

// ❌ Không tốt: State management thủ công
class UserScreen extends StatefulWidget {
@override
_UserScreenState createState() => _UserScreenState();
}

class _UserScreenState extends State<UserScreen> {
User? user;
bool isLoading = false;
String? error;

@override
void initState() {
super.initState();
_loadUser();
}

Future<void> _loadUser() async {
setState(() => isLoading = true);
try {
final response = await api.getUser(1);
setState(() {
user = response;
isLoading = false;
});
} catch (e) {
setState(() {
error = e.toString();
isLoading = false;
});
}
}

@override
Widget build(BuildContext context) {
// Phải xử lý loading, error, data trong mọi widget
if (isLoading) return CircularProgressIndicator();
if (error != null) return Text('Error: $error');
if (user == null) return Text('No data');
return UserWidget(user: user!);
}
}

Vấn đề:

  • ❌ Code lặp lại nhiều
  • ❌ Khó test
  • ❌ Khó share state giữa các widget
  • ❌ Không có caching

1.2 Giải Pháp: State Management

Với Riverpod/Provider:

  • ✅ Code sạch hơn
  • ✅ Dễ test
  • ✅ Share state dễ dàng
  • ✅ Có caching
  • ✅ Auto dispose

2️⃣ Sử Dụng Riverpod

2.1 Cài Đặt

dependencies:
flutter_riverpod: ^2.4.9
riverpod_annotation: ^2.3.3

dev_dependencies:
build_runner: ^2.4.7
riverpod_generator: ^2.3.9

2.2 Tạo API Provider

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart';

// API Service
final apiServiceProvider = Provider<Dio>((ref) {
return Dio(BaseOptions(
baseUrl: 'https://api.example.com',
));
});

// User Repository
final userRepositoryProvider = Provider<UserRepository>((ref) {
final dio = ref.watch(apiServiceProvider);
return UserRepository(dio);
});

2.3 Tạo Async State Provider

import 'package:flutter_riverpod/flutter_riverpod.dart';

// User Provider với async state
final userProvider = FutureProvider.family<User, int>((ref, userId) async {
final repository = ref.watch(userRepositoryProvider);
return await repository.getUser(userId);
});

// Users List Provider
final usersProvider = FutureProvider<List<User>>((ref) async {
final repository = ref.watch(userRepositoryProvider);
return await repository.getUsers();
});

2.4 Sử Dụng Trong Widget

import 'package:flutter_riverpod/flutter_riverpod.dart';

class UserScreen extends ConsumerWidget {
final int userId;

const UserScreen({Key? key, required this.userId}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(userId));

return Scaffold(
appBar: AppBar(title: Text('User')),
body: userAsync.when(
data: (user) => UserDetailsWidget(user: user),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorWidget(
error: error,
onRetry: () => ref.refresh(userProvider(userId)),
),
),
);
}
}

3️⃣ StateNotifier Cho Complex State

3.1 Tạo State Class

// User State
class UserState {
final User? user;
final bool isLoading;
final String? error;

UserState({
this.user,
this.isLoading = false,
this.error,
});

UserState copyWith({
User? user,
bool? isLoading,
String? error,
}) {
return UserState(
user: user ?? this.user,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
}

3.2 Tạo StateNotifier

import 'package:flutter_riverpod/flutter_riverpod.dart';

class UserNotifier extends StateNotifier<UserState> {
final UserRepository repository;

UserNotifier(this.repository) : super(UserState());

Future<void> loadUser(int userId) async {
state = state.copyWith(isLoading: true, error: null);

try {
final user = await repository.getUser(userId);
state = state.copyWith(user: user, isLoading: false);
} catch (e) {
state = state.copyWith(
error: e.toString(),
isLoading: false,
);
}
}

Future<void> updateUser(User user) async {
state = state.copyWith(isLoading: true, error: null);

try {
final updatedUser = await repository.updateUser(user);
state = state.copyWith(user: updatedUser, isLoading: false);
} catch (e) {
state = state.copyWith(
error: e.toString(),
isLoading: false,
);
}
}

void clearError() {
state = state.copyWith(error: null);
}
}

// Provider
final userNotifierProvider = StateNotifierProvider<UserNotifier, UserState>((ref) {
final repository = ref.watch(userRepositoryProvider);
return UserNotifier(repository);
});

3.3 Sử Dụng StateNotifier

class UserScreen extends ConsumerWidget {
final int userId;

const UserScreen({Key? key, required this.userId}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
final userState = ref.watch(userNotifierProvider);

// Load user khi widget được build
ref.listen(userNotifierProvider.notifier, (previous, next) {
next.loadUser(userId);
});

if (userState.isLoading) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}

if (userState.error != null) {
return Scaffold(
body: ErrorWidget(
error: userState.error!,
onRetry: () => ref.read(userNotifierProvider.notifier).loadUser(userId),
),
);
}

if (userState.user == null) {
return Scaffold(
body: Center(child: Text('No data')),
);
}

return Scaffold(
appBar: AppBar(title: Text('User')),
body: UserDetailsWidget(user: userState.user!),
);
}
}

4️⃣ AsyncNotifier (Riverpod 2.0+)

4.1 Tạo AsyncNotifier

import 'package:flutter_riverpod/flutter_riverpod.dart';

class UserAsyncNotifier extends AsyncNotifier<User> {
@override
Future<User> build() async {
// Load initial data
final userId = ref.watch(selectedUserIdProvider);
final repository = ref.watch(userRepositoryProvider);
return await repository.getUser(userId);
}

Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final userId = ref.read(selectedUserIdProvider);
final repository = ref.read(userRepositoryProvider);
return await repository.getUser(userId);
});
}

Future<void> updateUser(User user) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final repository = ref.read(userRepositoryProvider);
return await repository.updateUser(user);
});
}
}

// Provider
final userAsyncNotifierProvider =
AsyncNotifierProvider<UserAsyncNotifier, User>(() {
return UserAsyncNotifier();
});

4.2 Sử Dụng AsyncNotifier

class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userAsyncNotifierProvider);

return Scaffold(
appBar: AppBar(
title: Text('User'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => ref.read(userAsyncNotifierProvider.notifier).refresh(),
),
],
),
body: userAsync.when(
data: (user) => UserDetailsWidget(user: user),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorWidget(
error: error,
onRetry: () => ref.read(userAsyncNotifierProvider.notifier).refresh(),
),
),
);
}
}

5️⃣ Sử Dụng Provider (Alternative)

5.1 Cài Đặt Provider

dependencies:
provider: ^6.1.1

5.2 Tạo ChangeNotifier

import 'package:flutter/foundation.dart';

class UserProvider extends ChangeNotifier {
final UserRepository repository;

User? _user;
bool _isLoading = false;
String? _error;

User? get user => _user;
bool get isLoading => _isLoading;
String? get error => _error;

UserProvider(this.repository);

Future<void> loadUser(int userId) async {
_isLoading = true;
_error = null;
notifyListeners();

try {
_user = await repository.getUser(userId);
_error = null;
} catch (e) {
_error = e.toString();
_user = null;
} finally {
_isLoading = false;
notifyListeners();
}
}

Future<void> refresh() async {
if (_user != null) {
await loadUser(_user!.id);
}
}
}

5.3 Sử Dụng Provider

import 'package:provider/provider.dart';

class UserScreen extends StatelessWidget {
final int userId;

const UserScreen({Key? key, required this.userId}) : super(key: key);

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => UserProvider(UserRepository())..loadUser(userId),
child: Consumer<UserProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}

if (provider.error != null) {
return Scaffold(
body: ErrorWidget(
error: provider.error!,
onRetry: () => provider.loadUser(userId),
),
);
}

if (provider.user == null) {
return Scaffold(
body: Center(child: Text('No data')),
);
}

return Scaffold(
appBar: AppBar(title: Text('User')),
body: UserDetailsWidget(user: provider.user!),
);
},
),
);
}
}

6️⃣ Caching và Auto Refresh

6.1 Caching với Riverpod

// Provider với cache
final cachedUserProvider = FutureProvider.family<User, int>((ref, userId) async {
final repository = ref.watch(userRepositoryProvider);

// Cache sẽ tự động được quản lý bởi Riverpod
return await repository.getUser(userId);
});

// Auto refresh khi dependency thay đổi
final userWithAutoRefreshProvider = FutureProvider.family<User, int>((ref, userId) async {
// Watch một provider khác để trigger refresh
ref.watch(refreshTriggerProvider);

final repository = ref.watch(userRepositoryProvider);
return await repository.getUser(userId);
});

6.2 Manual Refresh

class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(1));

return Scaffold(
appBar: AppBar(
title: Text('User'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: () {
// Invalidate và reload
ref.invalidate(userProvider(1));
},
),
],
),
body: userAsync.when(
data: (user) => UserDetailsWidget(user: user),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorWidget(error: error),
),
);
}
}

7️⃣ Error Handling với Riverpod

7.1 Custom Error Handling

final userProvider = FutureProvider.family<User, int>((ref, userId) async {
try {
final repository = ref.watch(userRepositoryProvider);
return await repository.getUser(userId);
} on AppException catch (e) {
// Handle specific exceptions
throw e;
} catch (e) {
throw UnknownException('Failed to load user');
}
});

// Sử dụng
class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(1));

// Listen to errors
ref.listen(userProvider(1), (previous, next) {
next.whenOrNull(
error: (error, stack) {
if (error is AppException) {
ErrorHandler.handleError(error);
}
},
);
});

return userAsync.when(
data: (user) => UserDetailsWidget(user: user),
loading: () => CircularProgressIndicator(),
error: (error, stack) => ErrorWidget(error: error),
);
}
}

8️⃣ Ví Dụ Hoàn Chỉnh

8.1 Repository

class UserRepository {
final Dio dio;

UserRepository(this.dio);

Future<User> getUser(int id) async {
final response = await dio.get('/users/$id');
return User.fromJson(response.data as Map<String, dynamic>);
}

Future<List<User>> getUsers() async {
final response = await dio.get('/users');
return (response.data as List<dynamic>)
.map((json) => User.fromJson(json as Map<String, dynamic>))
.toList();
}

Future<User> createUser(User user) async {
final response = await dio.post('/users', data: user.toJson());
return User.fromJson(response.data as Map<String, dynamic>);
}
}

8.2 Providers

// API Service
final apiServiceProvider = Provider<Dio>((ref) {
return Dio(BaseOptions(baseUrl: 'https://api.example.com'));
});

// Repository
final userRepositoryProvider = Provider<UserRepository>((ref) {
final dio = ref.watch(apiServiceProvider);
return UserRepository(dio);
});

// User Provider
final userProvider = FutureProvider.family<User, int>((ref, userId) async {
final repository = ref.watch(userRepositoryProvider);
return await repository.getUser(userId);
});

// Users List Provider
final usersProvider = FutureProvider<List<User>>((ref) async {
final repository = ref.watch(userRepositoryProvider);
return await repository.getUsers();
});

8.3 UI

// User List Screen
class UserListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(usersProvider);

return Scaffold(
appBar: AppBar(
title: Text('Users'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => ref.invalidate(usersProvider),
),
],
),
body: usersAsync.when(
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index].name),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => UserScreen(userId: users[index].id),
),
);
},
);
},
),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorWidget(
error: error,
onRetry: () => ref.invalidate(usersProvider),
),
),
);
}
}

// User Detail Screen
class UserScreen extends ConsumerWidget {
final int userId;

const UserScreen({Key? key, required this.userId}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(userId));

return Scaffold(
appBar: AppBar(title: Text('User')),
body: userAsync.when(
data: (user) => UserDetailsWidget(user: user),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorWidget(
error: error,
onRetry: () => ref.invalidate(userProvider(userId)),
),
),
);
}
}

9️⃣ Best Practices

9.1 Tổ Chức Code

lib/
providers/
api_providers.dart
user_providers.dart
repositories/
user_repository.dart
models/
user.dart
screens/
user_screen.dart

9.2 Separation of Concerns

  • ✅ Repository: Xử lý API calls
  • ✅ Provider: Quản lý state
  • ✅ Widget: Hiển thị UI

9.3 Testing

// Test provider
void main() {
test('userProvider loads user correctly', () async {
final container = ProviderContainer();
final user = await container.read(userProvider(1).future);
expect(user.id, 1);
});
}

🔟 Kết Luận

Sử dụng Riverpod/Provider cho API state management giúp:

Code sạch hơn: Tách biệt logic và UI
Dễ test: Test providers độc lập
Caching tự động: Riverpod tự động cache
Auto dispose: Tự động cleanup
Type-safe: Compile-time safety

💡 Lời khuyên: Sử dụng Riverpod cho dự án mới. Nó mạnh mẽ hơn Provider và có nhiều tính năng tốt hơn. Provider vẫn tốt nếu bạn đã có codebase sử dụng nó.


🎓 Học Sâu Hơn Về Flutter

Muốn master Flutter, State Management, và các best practices? Tham gia các khóa học tại Hướng Nghiệp Dữ Liệu:

📚 Khóa Học Liên Quan:


📝 Bài viết này được biên soạn bởi đội ngũ Hướng Nghiệp Dữ Liệu. Để cập nhật thêm về Flutter, state management và các best practices trong phát triển ứng dụng di động, hãy theo dõi blog của chúng tôi.