Hướng Dẫn Sử Dụng Riverpod/Provider Cho Quản Lý State Khi Gọi API
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:
- ✅ Lập Trình Ứng Dụng Di Động Flutter - Xây dựng ứng dụng Flutter từ cơ bản đến nâng cao
📝 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.
