Quản lý State trong Flutter: Riverpod vs BLoC - Hướng dẫn So sánh Đầy đủ

So sánh chi tiết Riverpod và BLoC cho quản lý state trong Flutter. Kiến trúc, hiệu suất, khả năng kiểm thử và các trường hợp sử dụng để chọn giải pháp tốt nhất.

So sánh Riverpod và BLoC cho quản lý state trong Flutter

Quản lý state là một thách thức trung tâm trong phát triển Flutter. Riverpod và BLoC chiếm ưu thế trong hệ sinh thái, mỗi giải pháp đưa ra một triết lý riêng biệt. Hướng dẫn này so sánh hai giải pháp thông qua các triển khai cụ thể nhằm hỗ trợ lựa chọn theo nhu cầu dự án.

Yêu cầu trước

Hướng dẫn này giả định người đọc đã quen với Flutter và các kiến thức cơ bản về quản lý state. Các ví dụ sử dụng Riverpod 2.x và flutter_bloc 8.x, các phiên bản ổn định hiện tại.

Triết lý Cốt lõi của Hai Cách tiếp cận

Riverpod và BLoC giải quyết cùng một vấn đề bằng các mô hình đối lập. Hiểu rõ những khác biệt khái niệm này giúp lựa chọn công cụ phù hợp cho từng bối cảnh.

Riverpod áp dụng cách tiếp cận khai báo và phản ứng. Các provider định nghĩa nguồn dữ liệu mà widget quan sát. Framework tự động quản lý vòng đời, caching và phụ thuộc giữa các provider.

BLoC (Business Logic Component) buộc tuân theo kiến trúc nghiêm ngặt hướng sự kiện. Các thành phần phát ra event, Bloc xử lý và tạo ra state mới. Sự tách bạch rõ ràng này tạo điều kiện thuận lợi cho việc theo dõi luồng dữ liệu.

riverpod_philosophy.dartdart
// Riverpod: simple declaration, framework handles the rest
final counterProvider = StateProvider<int>((ref) => 0);

// Usage in a widget
class CounterWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // Reactive read: automatic rebuild if value changes
    final count = ref.watch(counterProvider);
    return Text('$count');
  }
}
bloc_philosophy.dartdart
// BLoC: explicit events/states separation
abstract class CounterEvent {}
class IncrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    // Each event has its dedicated handler
    on<IncrementPressed>((event, emit) => emit(state + 1));
  }
}

// Usage in a widget
class CounterWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return BlocBuilder<CounterBloc, int>(
      builder: (context, count) => Text('$count'),
    );
  }
}

Việc chọn giữa hai cách tiếp cận này phụ thuộc vào sở thích của đội nhóm và các ràng buộc của dự án.

Cấu hình Ban đầu

Cấu hình ban đầu bộc lộ những khác biệt về tính tiện dụng giữa hai giải pháp. Riverpod ưu tiên sự đơn giản, BLoC mang lại nhiều cấu trúc hơn.

Cài đặt Riverpod

Riverpod chỉ cần một gói duy nhất và một wrapper ở gốc ứng dụng. Code generation tùy chọn giúp cải thiện năng suất.

main.dartdart
// Riverpod configuration: single wrapper at root
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    // ProviderScope wraps the entire application
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}

Cài đặt BLoC

BLoC cần nhiều gói và cấu hình chi tiết hơn với BlocProvider cho mỗi Bloc được sử dụng.

main.dartdart
// BLoC configuration: explicit providers for each Bloc
import 'package:flutter_bloc/flutter_bloc.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    // MultiBlocProvider for multiple Blocs
    return MultiBlocProvider(
      providers: [
        BlocProvider(create: (_) => AuthBloc()),
        BlocProvider(create: (_) => ThemeBloc()),
      ],
      child: MaterialApp(
        home: HomeScreen(),
      ),
    );
  }
}

Cấu hình BLoC đòi hỏi nhiều mã ban đầu hơn nhưng làm rõ các phụ thuộc ngay từ đầu.

Quản lý State Đơn giản: Counter và Toggle

Các trường hợp đơn giản minh họa tính tiện dụng hằng ngày của mỗi giải pháp. Riverpod nổi bật ở sự ngắn gọn, BLoC giữ cấu trúc hướng sự kiện của nó.

Counter với Riverpod

counter_riverpod.dartdart
// StateProvider: simple state without complex logic
final counterProvider = StateProvider<int>((ref) => 0);

class CounterScreen extends ConsumerWidget {
  const CounterScreen({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    // watch for reactive value
    final count = ref.watch(counterProvider);

    return Scaffold(
      body: Center(child: Text('Count: $count')),
      floatingActionButton: FloatingActionButton(
        // read for actions (no rebuild)
        onPressed: () => ref.read(counterProvider.notifier).state++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

Counter với BLoC

counter_bloc.dartdart
// Typed events for each possible action
sealed class CounterEvent {}
class CounterIncremented extends CounterEvent {}
class CounterDecremented extends CounterEvent {}
class CounterReset extends CounterEvent {}

// Bloc with handlers for each event
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncremented>((event, emit) => emit(state + 1));
    on<CounterDecremented>((event, emit) => emit(state - 1));
    on<CounterReset>((event, emit) => emit(0));
  }
}

class CounterScreen extends StatelessWidget {
  const CounterScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, count) => Text('Count: $count'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // Event dispatch to modify state
        onPressed: () => context.read<CounterBloc>().add(CounterIncremented()),
        child: const Icon(Icons.add),
      ),
    );
  }
}

Với các trường hợp đơn giản, Riverpod giảm đáng kể boilerplate. BLoC trở nên phù hợp khi logic ngày càng phức tạp.

StateProvider vs StateNotifierProvider

StateProvider phù hợp với các giá trị nguyên thủy đơn giản. Với các đối tượng phức tạp hoặc logic nghiệp vụ, StateNotifierProvider hoặc NotifierProvider mang lại nhiều quyền kiểm soát hơn.

Quản lý State Bất đồng bộ: Gọi API

Các thao tác bất đồng bộ phơi bày sức mạnh của mỗi giải pháp. Quản lý các state loading, error và data là một thách thức quan trọng.

Dữ liệu Bất đồng bộ với Riverpod

async_riverpod.dartdart
// FutureProvider: automatic loading/error/data management
final usersProvider = FutureProvider.autoDispose<List<User>>((ref) async {
  final repository = ref.watch(userRepositoryProvider);
  // autoDispose releases resources when provider is no longer used
  return repository.fetchUsers();
});

class UsersScreen extends ConsumerWidget {
  const UsersScreen({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final usersAsync = ref.watch(usersProvider);

    // when handles all 3 possible states
    return usersAsync.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (error, stack) => Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Error: $error'),
            ElevatedButton(
              // invalidate forces reload
              onPressed: () => ref.invalidate(usersProvider),
              child: const Text('Retry'),
            ),
          ],
        ),
      ),
      data: (users) => ListView.builder(
        itemCount: users.length,
        itemBuilder: (context, index) => UserTile(user: users[index]),
      ),
    );
  }
}

Dữ liệu Bất đồng bộ với BLoC

async_bloc.dartdart
// Explicit states for each loading phase
sealed class UsersState {}
class UsersInitial extends UsersState {}
class UsersLoading extends UsersState {}
class UsersLoaded extends UsersState {
  final List<User> users;
  UsersLoaded(this.users);
}
class UsersError extends UsersState {
  final String message;
  UsersError(this.message);
}

// Events to trigger actions
sealed class UsersEvent {}
class UsersFetchRequested extends UsersEvent {}
class UsersRefreshRequested extends UsersEvent {}

class UsersBloc extends Bloc<UsersEvent, UsersState> {
  final UserRepository _repository;

  UsersBloc(this._repository) : super(UsersInitial()) {
    on<UsersFetchRequested>(_onFetchRequested);
    on<UsersRefreshRequested>(_onRefreshRequested);
  }

  Future<void> _onFetchRequested(
    UsersFetchRequested event,
    Emitter<UsersState> emit,
  ) async {
    emit(UsersLoading());
    try {
      final users = await _repository.fetchUsers();
      emit(UsersLoaded(users));
    } catch (e) {
      emit(UsersError(e.toString()));
    }
  }

  Future<void> _onRefreshRequested(
    UsersRefreshRequested event,
    Emitter<UsersState> emit,
  ) async {
    // Keep current state during refresh
    final currentState = state;
    try {
      final users = await _repository.fetchUsers();
      emit(UsersLoaded(users));
    } catch (e) {
      // Restore previous state on error
      if (currentState is UsersLoaded) {
        emit(currentState);
      } else {
        emit(UsersError(e.toString()));
      }
    }
  }
}
users_screen_bloc.dartdart
// Widget with pattern matching on states
class UsersScreen extends StatelessWidget {
  const UsersScreen({super.key});

  
  Widget build(BuildContext context) {
    return BlocBuilder<UsersBloc, UsersState>(
      builder: (context, state) {
        return switch (state) {
          UsersInitial() => const Center(
              child: ElevatedButton(
                onPressed: _fetchUsers,
                child: Text('Load'),
              ),
            ),
          UsersLoading() => const Center(child: CircularProgressIndicator()),
          UsersError(:final message) => Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Error: $message'),
                  ElevatedButton(
                    onPressed: () => context
                        .read<UsersBloc>()
                        .add(UsersFetchRequested()),
                    child: const Text('Retry'),
                  ),
                ],
              ),
            ),
          UsersLoaded(:final users) => ListView.builder(
              itemCount: users.length,
              itemBuilder: (context, index) => UserTile(user: users[index]),
            ),
        };
      },
    );
  }

  void _fetchUsers(BuildContext context) {
    context.read<UsersBloc>().add(UsersFetchRequested());
  }
}

BLoC mang lại sự kiểm soát chi tiết với mỗi chuyển đổi state. Riverpod tự động hóa nhiều hơn thông qua AsyncValue.

Sẵn sàng chinh phục phỏng vấn Flutter?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Phụ thuộc giữa các State: Hợp thành và Tiêm phụ thuộc

Ứng dụng thực tế bao gồm các state phụ thuộc lẫn nhau. Việc quản lý các phụ thuộc này tạo ra sự khác biệt lớn giữa hai cách tiếp cận.

Hợp thành với Riverpod

composition_riverpod.dartdart
// Base provider: configuration
final apiClientProvider = Provider<ApiClient>((ref) {
  final baseUrl = ref.watch(environmentProvider).apiUrl;
  return ApiClient(baseUrl: baseUrl);
});

// Dependent provider: repository
final productRepositoryProvider = Provider<ProductRepository>((ref) {
  // Automatic client injection
  final client = ref.watch(apiClientProvider);
  return ProductRepository(client);
});

// Provider with parameter: product by ID
final productProvider = FutureProvider.autoDispose.family<Product, String>(
  (ref, productId) async {
    final repository = ref.watch(productRepositoryProvider);
    return repository.getProduct(productId);
  },
);

// Derived provider: filtered products
final filteredProductsProvider = Provider<List<Product>>((ref) {
  final products = ref.watch(productsProvider).valueOrNull ?? [];
  final filter = ref.watch(productFilterProvider);

  return products.where((p) => p.category == filter.category).toList();
});

// Usage with parameter
class ProductDetailScreen extends ConsumerWidget {
  final String productId;

  const ProductDetailScreen({super.key, required this.productId});

  
  Widget build(BuildContext context, WidgetRef ref) {
    // family allows passing parameters
    final productAsync = ref.watch(productProvider(productId));

    return productAsync.when(
      loading: () => const ProductSkeleton(),
      error: (e, _) => ErrorWidget(error: e),
      data: (product) => ProductDetails(product: product),
    );
  }
}

Hợp thành với BLoC

composition_bloc.dartdart
// Repository injected into the Bloc
class ProductBloc extends Bloc<ProductEvent, ProductState> {
  final ProductRepository _repository;
  final CartBloc _cartBloc;
  late final StreamSubscription _cartSubscription;

  ProductBloc({
    required ProductRepository repository,
    required CartBloc cartBloc,
  })  : _repository = repository,
        _cartBloc = cartBloc,
        super(ProductInitial()) {
    on<ProductFetchRequested>(_onFetchRequested);
    on<ProductAddedToCart>(_onAddedToCart);

    // Listen to cart changes
    _cartSubscription = _cartBloc.stream.listen((cartState) {
      // React to cart changes
      if (cartState is CartUpdated) {
        add(ProductCartSyncRequested(cartState.items));
      }
    });
  }

  Future<void> _onFetchRequested(
    ProductFetchRequested event,
    Emitter<ProductState> emit,
  ) async {
    emit(ProductLoading());
    try {
      final product = await _repository.getProduct(event.productId);
      // Check if product is in cart
      final isInCart = _cartBloc.state.contains(product.id);
      emit(ProductLoaded(product, isInCart: isInCart));
    } catch (e) {
      emit(ProductError(e.toString()));
    }
  }

  
  Future<void> close() {
    _cartSubscription.cancel();
    return super.close();
  }
}

// Configuration with dependency injection
class ProductsPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => ProductBloc(
        repository: context.read<ProductRepository>(),
        cartBloc: context.read<CartBloc>(),
      )..add(ProductFetchRequested()),
      child: const ProductsView(),
    );
  }
}

Riverpod quản lý các phụ thuộc theo cách khai báo. BLoC yêu cầu quản lý thủ công các subscription giữa các Bloc.

Khả năng Kiểm thử và Mocking

Kiểm thử là một tiêu chí quyết định cho các dự án chuyên nghiệp. Cả hai giải pháp đều xuất sắc trong lĩnh vực này với cách tiếp cận khác nhau.

Kiểm thử với Riverpod

test_riverpod.dartdart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  group('UserProvider Tests', () {
    late MockUserRepository mockRepository;
    late ProviderContainer container;

    setUp(() {
      mockRepository = MockUserRepository();
      // Isolated container with override
      container = ProviderContainer(
        overrides: [
          userRepositoryProvider.overrideWithValue(mockRepository),
        ],
      );
    });

    tearDown(() => container.dispose());

    test('returns users from repository', () async {
      // Arrange
      final expectedUsers = [User(id: '1', name: 'Test')];
      when(() => mockRepository.fetchUsers())
          .thenAnswer((_) async => expectedUsers);

      // Act
      final users = await container.read(usersProvider.future);

      // Assert
      expect(users, expectedUsers);
      verify(() => mockRepository.fetchUsers()).called(1);
    });

    test('handles repository errors', () async {
      when(() => mockRepository.fetchUsers())
          .thenThrow(Exception('Network error'));

      expect(
        () => container.read(usersProvider.future),
        throwsException,
      );
    });
  });
}

Kiểm thử với BLoC

test_bloc.dartdart
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  group('UsersBloc Tests', () {
    late MockUserRepository mockRepository;

    setUp(() {
      mockRepository = MockUserRepository();
    });

    // blocTest simplifies state sequence testing
    blocTest<UsersBloc, UsersState>(
      'emits [Loading, Loaded] when fetch succeeds',
      build: () {
        when(() => mockRepository.fetchUsers())
            .thenAnswer((_) async => [User(id: '1', name: 'Test')]);
        return UsersBloc(mockRepository);
      },
      act: (bloc) => bloc.add(UsersFetchRequested()),
      expect: () => [
        isA<UsersLoading>(),
        isA<UsersLoaded>().having(
          (s) => s.users.length,
          'users count',
          1,
        ),
      ],
    );

    blocTest<UsersBloc, UsersState>(
      'emits [Loading, Error] when fetch fails',
      build: () {
        when(() => mockRepository.fetchUsers())
            .thenThrow(Exception('Network error'));
        return UsersBloc(mockRepository);
      },
      act: (bloc) => bloc.add(UsersFetchRequested()),
      expect: () => [
        isA<UsersLoading>(),
        isA<UsersError>(),
      ],
    );
  });
}

Gói bloc_test cung cấp cú pháp dành riêng để kiểm thử chuỗi state. Riverpod sử dụng các pattern kiểm thử Flutter tiêu chuẩn.

Phạm vi Kiểm thử

Chỉ kiểm thử các trường hợp danh nghĩa là không đủ. Bài kiểm thử cần bao quát các lỗi mạng, timeout, các state biên và các chuyển đổi state ngoài dự kiến.

Hiệu suất và Tối ưu hóa Rebuild

Hiệu suất ảnh hưởng trực tiếp đến trải nghiệm người dùng. Cả hai giải pháp đều cung cấp các cơ chế tối ưu khác nhau.

Tối ưu hóa với Riverpod

perf_riverpod.dartdart
// select to rebuild only if targeted value changes
class UserNameWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // Rebuilds only if user.name changes
    final name = ref.watch(userProvider.select((user) => user.name));
    return Text(name);
  }
}

// Provider with automatic caching
final expensiveComputationProvider = Provider<ExpensiveResult>((ref) {
  final input = ref.watch(inputProvider);
  // Computation automatically cached
  return performExpensiveComputation(input);
});

// autoDispose to release unused resources
final searchResultsProvider = FutureProvider.autoDispose
    .family<List<Product>, String>((ref, query) async {
  // Temporary keepAlive during typing
  final link = ref.keepAlive();

  // Timer to release after inactivity
  final timer = Timer(const Duration(seconds: 30), link.close);
  ref.onDispose(timer.cancel);

  return searchProducts(query);
});

Tối ưu hóa với BLoC

perf_bloc.dartdart
// buildWhen limits rebuilds conditionally
class UserNameWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return BlocBuilder<UserBloc, UserState>(
      // Rebuilds only if name changes
      buildWhen: (previous, current) {
        if (previous is UserLoaded && current is UserLoaded) {
          return previous.user.name != current.user.name;
        }
        return true;
      },
      builder: (context, state) {
        if (state is UserLoaded) {
          return Text(state.user.name);
        }
        return const SizedBox.shrink();
      },
    );
  }
}

// BlocSelector to extract a specific value
class UserAvatarWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return BlocSelector<UserBloc, UserState, String?>(
      // Select only the avatar URL
      selector: (state) => state is UserLoaded ? state.user.avatarUrl : null,
      builder: (context, avatarUrl) {
        if (avatarUrl == null) return const DefaultAvatar();
        return NetworkImage(avatarUrl);
      },
    );
  }
}

Cả hai giải pháp đều cung cấp các tối ưu chi tiết. Riverpod với select, BLoC với buildWhenBlocSelector.

Trường hợp Thực tế: Xác thực Hoàn chỉnh

Một hệ thống xác thực minh họa các pattern thực tế của mỗi giải pháp. Trường hợp này kết hợp state lâu dài, gọi API và điều hướng.

Xác thực với Riverpod

auth_riverpod.dartdart
// Authentication state with sealed class
sealed class AuthState {
  const AuthState();
}
class AuthInitial extends AuthState {
  const AuthInitial();
}
class AuthLoading extends AuthState {
  const AuthLoading();
}
class AuthAuthenticated extends AuthState {
  final User user;
  const AuthAuthenticated(this.user);
}
class AuthUnauthenticated extends AuthState {
  final String? error;
  const AuthUnauthenticated([this.error]);
}

// Notifier to manage auth state
class AuthNotifier extends StateNotifier<AuthState> {
  final AuthRepository _repository;
  final SecureStorage _storage;

  AuthNotifier(this._repository, this._storage) : super(const AuthInitial()) {
    _checkAuthStatus();
  }

  Future<void> _checkAuthStatus() async {
    final token = await _storage.getToken();
    if (token != null) {
      try {
        final user = await _repository.getCurrentUser(token);
        state = AuthAuthenticated(user);
      } catch (_) {
        await _storage.deleteToken();
        state = const AuthUnauthenticated();
      }
    } else {
      state = const AuthUnauthenticated();
    }
  }

  Future<void> login(String email, String password) async {
    state = const AuthLoading();
    try {
      final result = await _repository.login(email, password);
      await _storage.saveToken(result.token);
      state = AuthAuthenticated(result.user);
    } catch (e) {
      state = AuthUnauthenticated(e.toString());
    }
  }

  Future<void> logout() async {
    await _storage.deleteToken();
    state = const AuthUnauthenticated();
  }
}

// Provider with injected dependencies
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
  return AuthNotifier(
    ref.watch(authRepositoryProvider),
    ref.watch(secureStorageProvider),
  );
});

// Redirect based on auth state
final routerProvider = Provider<GoRouter>((ref) {
  final authState = ref.watch(authProvider);

  return GoRouter(
    redirect: (context, state) {
      final isAuth = authState is AuthAuthenticated;
      final isAuthRoute = state.matchedLocation.startsWith('/auth');

      if (!isAuth && !isAuthRoute) return '/auth/login';
      if (isAuth && isAuthRoute) return '/home';
      return null;
    },
    routes: [...],
  );
});

Xác thực với BLoC

auth_bloc.dartdart
// Exhaustive states for authentication
sealed class AuthState {
  const AuthState();
}
class AuthInitial extends AuthState {
  const AuthInitial();
}
class AuthCheckInProgress extends AuthState {
  const AuthCheckInProgress();
}
class AuthLoginInProgress extends AuthState {
  const AuthLoginInProgress();
}
class AuthSuccess extends AuthState {
  final User user;
  const AuthSuccess(this.user);
}
class AuthFailure extends AuthState {
  final String error;
  const AuthFailure(this.error);
}
class AuthLoggedOut extends AuthState {
  const AuthLoggedOut();
}

// Authentication events
sealed class AuthEvent {
  const AuthEvent();
}
class AuthCheckRequested extends AuthEvent {
  const AuthCheckRequested();
}
class AuthLoginSubmitted extends AuthEvent {
  final String email;
  final String password;
  const AuthLoginSubmitted(this.email, this.password);
}
class AuthLogoutRequested extends AuthEvent {
  const AuthLogoutRequested();
}

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final AuthRepository _repository;
  final SecureStorage _storage;

  AuthBloc({
    required AuthRepository repository,
    required SecureStorage storage,
  })  : _repository = repository,
        _storage = storage,
        super(const AuthInitial()) {
    on<AuthCheckRequested>(_onCheckRequested);
    on<AuthLoginSubmitted>(_onLoginSubmitted);
    on<AuthLogoutRequested>(_onLogoutRequested);
  }

  Future<void> _onCheckRequested(
    AuthCheckRequested event,
    Emitter<AuthState> emit,
  ) async {
    emit(const AuthCheckInProgress());
    final token = await _storage.getToken();

    if (token == null) {
      emit(const AuthLoggedOut());
      return;
    }

    try {
      final user = await _repository.getCurrentUser(token);
      emit(AuthSuccess(user));
    } catch (_) {
      await _storage.deleteToken();
      emit(const AuthLoggedOut());
    }
  }

  Future<void> _onLoginSubmitted(
    AuthLoginSubmitted event,
    Emitter<AuthState> emit,
  ) async {
    emit(const AuthLoginInProgress());
    try {
      final result = await _repository.login(event.email, event.password);
      await _storage.saveToken(result.token);
      emit(AuthSuccess(result.user));
    } catch (e) {
      emit(AuthFailure(e.toString()));
    }
  }

  Future<void> _onLogoutRequested(
    AuthLogoutRequested event,
    Emitter<AuthState> emit,
  ) async {
    await _storage.deleteToken();
    emit(const AuthLoggedOut());
  }
}

Cả hai triển khai đều xử lý cùng chức năng nhưng theo các phong cách khác nhau. BLoC làm rõ từng chuyển đổi, Riverpod đơn giản hóa cú pháp.

Bảng Tóm tắt So sánh

| Tiêu chí | Riverpod | BLoC | |----------|----------|------| | Đường cong học tập | Vừa phải | Dốc hơn | | Boilerplate | Tối thiểu | Đáng kể | | Type Safety | Xuất sắc | Xuất sắc | | Khả năng kiểm thử | Xuất sắc | Xuất sắc | | Khả năng truy vết | Qua DevTools | Events/States rõ ràng | | Hợp thành | Tự động | Thủ công | | Code generation | Tùy chọn | Không bắt buộc | | Quy mô đội nhóm | Linh hoạt | Đội lớn |

Khuyến nghị theo Bối cảnh

Lựa chọn giữa Riverpod và BLoC phụ thuộc vào nhiều yếu tố bối cảnh.

Chọn Riverpod khi:

  • Đội nhóm ưu tiên sự ngắn gọn và năng suất
  • Dự án cần hợp thành state linh hoạt
  • Lập trình viên đến từ React hoặc các framework phản ứng khác
  • Caching tự động là một lợi thế đáng kể

Chọn BLoC khi:

  • Đội nhóm trân trọng các pattern nghiêm ngặt và có thể dự đoán
  • Dự án cần truy vết đầy đủ các sự kiện
  • Các thành viên junior hưởng lợi từ kiến trúc bị ràng buộc
  • Việc gỡ lỗi cần lịch sử chuyển đổi

Kết luận

Riverpod và BLoC đáp ứng hiệu quả nhu cầu quản lý state trong Flutter. Riverpod nổi bật về tính tiện dụng và linh hoạt, BLoC về cấu trúc và khả năng dự đoán. Cả hai giải pháp đều cung cấp khả năng kiểm thử xuất sắc và hiệu suất tối ưu.

Danh sách Kiểm tra Quyết định

  • ✅ Đánh giá quy mô và kinh nghiệm của đội nhóm
  • ✅ Xem xét độ phức tạp của luồng dữ liệu
  • ✅ Phân tích nhu cầu truy vết và gỡ lỗi
  • ✅ Thử cả hai giải pháp trên một nguyên mẫu
  • ✅ Kiểm tra tính nhất quán với kiến trúc hiện có

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Lựa chọn tốt nhất vẫn là giải pháp mà đội nhóm thành thạo và bảo trì hiệu quả. Sự nhất quán trong áp dụng quan trọng hơn việc lựa chọn giải pháp.

Thẻ

#flutter
#riverpod
#bloc
#state management
#dart

Chia sẻ

Bài viết liên quan