So sánh các phương pháp quản lý trạng thái trong Flutter

Provider vs. Riverpod vs. Bloc vs. GetX

So sánh phương pháp quản lý trạng thái Flutter

Đăng ngày: 05/03/2024 · Thời gian đọc: 15 phút

Quản lý trạng thái (state management) là một trong những khía cạnh quan trọng nhất trong phát triển ứng dụng Flutter. Khi ứng dụng phát triển và trở nên phức tạp hơn, việc lựa chọn giải pháp quản lý trạng thái phù hợp trở thành yếu tố quyết định thành công của dự án. Hiện nay, Flutter cung cấp nhiều giải pháp quản lý trạng thái, trong đó bốn giải pháp phổ biến nhất là Provider, Riverpod, Bloc và GetX. Bài viết này sẽ cung cấp phân tích chi tiết và so sánh các giải pháp này để giúp bạn đưa ra lựa chọn phù hợp nhất cho dự án của mình.

Tổng quan về các giải pháp quản lý trạng thái

So sánh phương pháp quản lý trạng thái Flutter

Trước khi đi vào so sánh chi tiết, hãy cùng điểm qua tổng quan về mỗi giải pháp quản lý trạng thái:

Provider

Provider là một giải pháp quản lý trạng thái đơn giản, được Flutter team khuyên dùng. Dựa trên InheritedWidget của Flutter, Provider cung cấp một cách tiếp cận dễ hiểu để truyền và cập nhật dữ liệu qua widget tree.

Riverpod

Riverpod được phát triển bởi Remi Rousselet (cũng là tác giả của Provider) và được coi là "Provider 2.0". Riverpod giải quyết một số hạn chế của Provider như việc phụ thuộc vào BuildContext và khả năng ghép nối providers với nhau.

Bloc (Business Logic Component)

Bloc là một mẫu thiết kế (pattern) giúp tách biệt giao diện người dùng khỏi logic nghiệp vụ. Bloc sử dụng Streams và các sự kiện (events) để xử lý luồng dữ liệu một chiều từ UI đến state và ngược lại.

GetX

GetX là một giải pháp "tất cả trong một" (all-in-one), không chỉ cung cấp quản lý trạng thái mà còn bao gồm quản lý điều hướng (navigation), tiêm phụ thuộc (dependency injection) và nhiều tiện ích khác.

So sánh chi tiết dựa trên các tiêu chí

1. Độ phức tạp và đường cong học tập

Provider

  • Độ phức tạp: Thấp đến trung bình
  • Đường cong học tập: Dễ bắt đầu, khá trực quan
// Định nghĩa một ChangeNotifier
class Counter extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

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

// Sử dụng trong UI
ChangeNotifierProvider(
  create: (context) => Counter(),
  child: Builder(
    builder: (context) {
      return Column(
        children: [
          Text('Count: ${context.watch<Counter>().count}'),
          ElevatedButton(
            onPressed: () => context.read<Counter>().increment(),
            child: Text('Increment'),
          ),
        ],
      );
    },
  ),
)

Riverpod

  • Độ phức tạp: Trung bình
  • Đường cong học tập: Cần thời gian để làm quen, nhưng mang lại nhiều lợi ích
// Định nghĩa providers
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() => state = state + 1;
}

// Sử dụng trong UI
Consumer(
  builder: (context, ref, child) {
    final count = ref.watch(counterProvider);
    return Column(
      children: [
        Text('Count: $count'),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).increment(),
          child: Text('Increment'),
        ),
      ],
    );
  },
)

Bloc

  • Độ phức tạp: Cao
  • Đường cong học tập: Dốc, yêu cầu hiểu biết về luồng dữ liệu phản ứng (reactive data streams)
// Định nghĩa Events
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}

// Định nghĩa Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<IncrementEvent>((event, emit) {
      emit(state + 1);
    });
  }
}

// Sử dụng trong UI
BlocProvider(
  create: (context) => CounterBloc(),
  child: BlocBuilder<CounterBloc, int>(
    builder: (context, state) {
      return Column(
        children: [
          Text('Count: $state'),
          ElevatedButton(
            onPressed: () => 
                context.read<CounterBloc>().add(IncrementEvent()),
            child: Text('Increment'),
          ),
        ],
      );
    },
  ),
)

GetX

  • Độ phức tạp: Thấp đến trung bình
  • Đường cong học tập: Dễ bắt đầu, nhưng cần hiểu rõ quy ước và cấu trúc của GetX
// Định nghĩa Controller
class CounterController extends GetxController {
  var count = 0.obs;

  void increment() => count++;
}

// Sử dụng trong UI
GetX<CounterController>(
  init: CounterController(),
  builder: (controller) {
    return Column(
      children: [
        Text('Count: ${controller.count.value}'),
        ElevatedButton(
          onPressed: () => controller.increment(),
          child: Text('Increment'),
        ),
      ],
    );
  },
)

2. Hiệu suất

Provider

  • Hiệu suất rebuild: Trung bình. Provider sẽ rebuild tất cả các widget lắng nghe khi dữ liệu thay đổi, trừ khi bạn sử dụng Selector để tinh chỉnh.
  • Tải bộ nhớ: Nhẹ, không yêu cầu nhiều dependencies.
  • Khả năng mở rộng: Tốt cho các ứng dụng nhỏ đến trung bình. Có thể gặp vấn đề khi ứng dụng phát triển rất lớn.

Riverpod

  • Hiệu suất rebuild: Tốt. Riverpod cung cấp khả năng rebuild có chọn lọc thông qua tính năng select.
  • Tải bộ nhớ: Nhẹ đến trung bình, tương tự Provider.
  • Khả năng mở rộng: Rất tốt, được thiết kế để xử lý ứng dụng lớn và phức tạp.

Bloc

  • Hiệu suất rebuild: Tốt. Bloc cho phép kiểm soát tinh tế hơn với việc rebuild.
  • Tải bộ nhớ: Trung bình đến cao, do sử dụng Streams.
  • Khả năng mở rộng: Xuất sắc cho các ứng dụng phức tạp và lớn, nhờ vào cấu trúc rõ ràng.

GetX

  • Hiệu suất rebuild: Rất tốt. GetX chỉ rebuild những widget cần thiết.
  • Tải bộ nhớ: Trung bình đến cao, do cung cấp nhiều tính năng.
  • Khả năng mở rộng: Tốt cho ứng dụng vừa và nhỏ, nhưng có thể gặp khó khăn trong việc duy trì cấu trúc khi ứng dụng phát triển rất lớn.

3. Tích hợp với Flutter

Giải phápTích hợp với FlutterHỗ trợ cộng đồngFlutter Dev Tools
ProviderRất tốt, được Flutter team chính thức khuyên dùng.Mạnh mẽ, có nhiều tài liệu và ví dụ.Tích hợp tốt, dễ debug.
RiverpodTốt, nhưng cần cài đặt thêm dependency.Ngày càng phát triển, nhưng ít hơn Provider.Hỗ trợ tốt.
BlocTốt, được cộng đồng chấp nhận rộng rãi.Mạnh mẽ, có nhiều tài liệu và thư viện hỗ trợ.Có extension riêng để debug.
GetXTốt, nhưng có một số phương pháp không theo chuẩn Flutter.Phát triển nhanh, phổ biến trong cộng đồng quốc tế.Hỗ trợ cơ bản.

4. Khả năng kiểm thử (Testability)

Giải phápUnit TestingWidget TestingCoverage
ProviderDễ thực hiện.Đơn giản, dễ tích hợp.Tốt.
RiverpodRất dễ dàng, do tính chất độc lập với BuildContext.Đơn giản, với khả năng override providers.Xuất sắc.
BlocRất dễ dàng, do tách biệt logic khỏi UI.Cần setup nhiều hơn, nhưng cung cấp sự tách biệt tốt.Xuất sắc.
GetXTrung bình, do phụ thuộc vào nhiều thành phần của GetX.Khá phức tạp, cần setup nhiều.Trung bình.

5. Khả năng mở rộng và bảo trì

Giải phápKhả năng tái sử dụngKhả năng mở rộngBảo trì lâu dài
ProviderTốt.Trung bình, có thể trở nên phức tạp khi ứng dụng phát triển.Tốt, nhờ vào cấu trúc đơn giản.
RiverpodXuất sắc, với khả năng kết hợp và ghi đè providers.Xuất sắc, được thiết kế để xử lý các ứng dụng phức tạp.Tốt, với cấu trúc rõ ràng và type safety.
BlocTốt, với mẫu thiết kế rõ ràng.Xuất sắc, phù hợp với các ứng dụng lớn và phức tạp.Tốt, nhưng đòi hỏi tuân thủ nghiêm ngặt các quy tắc.
GetXTốt.Trung bình, có thể gặp khó khăn trong các ứng dụng rất lớn.Trung bình, do cách tiếp cận "ma thuật" có thể gây khó hiểu cho người mới vào dự án.

So sánh thông qua ví dụ thực tế

Ví dụ 1: Tương tác với API

Provider

class ProductsProvider extends ChangeNotifier {
  List<Product> _products = [];
  bool _isLoading = false;
  String? _error;

  List<Product> get products => _products;
  bool get isLoading => _isLoading;
  String? get error => _error;

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

    try {
      final response = await http.get(Uri.parse('https://api.example.com/products'));
      if (response.statusCode == 200) {
        _products = (json.decode(response.body) as List)
            .map((data) => Product.fromJson(data))
            .toList();
      } else {
        _error = 'Failed to load products';
      }
    } catch (e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

Riverpod

final productsProvider = StateNotifierProvider<ProductsNotifier, ProductsState>((ref) {
  return ProductsNotifier();
});

class ProductsState {
  final List<Product> products;
  final bool isLoading;
  final String? error;

  ProductsState({
    this.products = const [],
    this.isLoading = false,
    this.error,
  });

  ProductsState copyWith({
    List<Product>? products,
    bool? isLoading,
    String? error,
  }) {
    return ProductsState(
      products: products ?? this.products,
      isLoading: isLoading ?? this.isLoading,
      error: error,
    );
  }
}

class ProductsNotifier extends StateNotifier<ProductsState> {
  ProductsNotifier() : super(ProductsState()) {
    fetchProducts();
  }

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

    try {
      final response = await http.get(Uri.parse('https://api.example.com/products'));
      if (response.statusCode == 200) {
        final products = (json.decode(response.body) as List)
            .map((data) => Product.fromJson(data))
            .toList();
        state = state.copyWith(products: products, isLoading: false);
      } else {
        state = state.copyWith(
          error: 'Failed to load products',
          isLoading: false,
        );
      }
    } catch (e) {
      state = state.copyWith(error: e.toString(), isLoading: false);
    }
  }
}

Bloc

// Events
abstract class ProductsEvent {}
class FetchProductsEvent extends ProductsEvent {}

// States
abstract class ProductsState {}
class ProductsInitial extends ProductsState {}
class ProductsLoading extends ProductsState {}
class ProductsLoaded extends ProductsState {
  final List<Product> products;
  ProductsLoaded(this.products);
}
class ProductsError extends ProductsState {
  final String message;
  ProductsError(this.message);
}

// Bloc
class ProductsBloc extends Bloc<ProductsEvent, ProductsState> {
  final ProductsRepository repository;

  ProductsBloc({required this.repository}) : super(ProductsInitial()) {
    on<FetchProductsEvent>(_onFetchProducts);
  }

  Future<void> _onFetchProducts(
    FetchProductsEvent event,
    Emitter<ProductsState> emit,
  ) async {
    emit(ProductsLoading());
    try {
      final products = await repository.fetchProducts();
      emit(ProductsLoaded(products));
    } catch (e) {
      emit(ProductsError(e.toString()));
    }
  }
}

GetX

class ProductsController extends GetxController {
  var products = <Product>[].obs;
  var isLoading = false.obs;
  var error = RxString('');

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

  Future<void> fetchProducts() async {
    isLoading.value = true;
    error.value = '';

    try {
      final response = await http.get(Uri.parse('https://api.example.com/products'));
      if (response.statusCode == 200) {
        final productsList = (json.decode(response.body) as List)
            .map((data) => Product.fromJson(data))
            .toList();
        products.value = productsList;
      } else {
        error.value = 'Failed to load products';
      }
    } catch (e) {
      error.value = e.toString();
    } finally {
      isLoading.value = false;
    }
  }
}

So sánh tổng quát và đề xuất sử dụng

Provider

  • Điểm mạnh: Đơn giản, dễ học, được Flutter team khuyên dùng
  • Điểm yếu: Khả năng tái cấu trúc kém hơn khi ứng dụng phát triển lớn
  • Phù hợp với: Ứng dụng nhỏ đến trung bình, các nhóm phát triển mới làm quen với Flutter
  • Khi nào nên dùng: Khi bạn cần một giải pháp đơn giản và đủ tốt, khi dự án có thời gian phát triển ngắn

Riverpod

  • Điểm mạnh: Type safety, khả năng compose providers, không phụ thuộc vào BuildContext
  • Điểm yếu: Đường cong học tập dốc hơn Provider một chút
  • Phù hợp với: Ứng dụng có quy mô từ trung bình đến lớn, các nhóm có kinh nghiệm với Flutter
  • Khi nào nên dùng: Khi bạn cần tính linh hoạt cao, khả năng mở rộng tốt, và ưu tiên code dễ bảo trì

Bloc

  • Điểm mạnh: Cấu trúc rõ ràng, khả năng phân tách logic nghiệp vụ khỏi UI, dễ kiểm thử
  • Điểm yếu: Nhiều boilerplate code, đường cong học tập dốc
  • Phù hợp với: Ứng dụng lớn, phức tạp, yêu cầu khả năng bảo trì cao
  • Khi nào nên dùng: Khi dự án có nhiều nhà phát triển và cần một cấu trúc rõ ràng, khi logic nghiệp vụ phức tạp

GetX

  • Điểm mạnh: Đơn giản, ít boilerplate, nhiều tính năng tích hợp
  • Điểm yếu: Không tuân theo các quy ước Flutter tiêu chuẩn, có thể khó bảo trì trong dự án lớn
  • Phù hợp với: Ứng dụng vừa và nhỏ, các nhà phát triển cần phát triển nhanh
  • Khi nào nên dùng: Khi bạn cần phát triển nhanh và không muốn cấu hình nhiều, khi bạn làm việc một mình hoặc trong nhóm nhỏ

Kết luận

Không có giải pháp quản lý trạng thái nào là hoàn hảo cho mọi dự án Flutter. Lựa chọn phụ thuộc vào nhiều yếu tố như quy mô dự án, độ phức tạp, kinh nghiệm của nhóm phát triển, và các yêu cầu cụ thể của ứng dụng.

  • Provider là lựa chọn tốt cho người mới bắt đầu với Flutter và các dự án nhỏ đến trung bình.
  • Riverpod là bước tiến hóa từ Provider, phù hợp với các dự án trung bình đến lớn, đặc biệt khi bạn muốn code dễ bảo trì và mở rộng.
  • Bloc là giải pháp mạnh mẽ cho các ứng dụng lớn và phức tạp, nhưng đòi hỏi thời gian học tập và hiểu biết sâu.
  • GetX cung cấp giải pháp nhanh chóng và dễ dàng, phù hợp với phát triển nhanh và các ứng dụng vừa và nhỏ.

Trong thực tế, nhiều dự án có thể kết hợp nhiều giải pháp quản lý trạng thái khác nhau, tùy thuộc vào nhu cầu cụ thể của từng phần trong ứng dụng. Điều quan trọng là hiểu rõ ưu và nhược điểm của mỗi giải pháp để đưa ra lựa chọn phù hợp nhất cho dự án của bạn.

Bạn đang sử dụng giải pháp quản lý trạng thái nào cho ứng dụng Flutter của mình? Bạn có đánh giá như thế nào về những trải nghiệm của mình với các giải pháp này? Hãy chia sẻ trong phần bình luận bên dưới!