Riverpod, BLoC và Provider trong Flutter

🧠 State Management in Flutter: Choosing Between Bloc, Riverpod, and Provider - DEV Community

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

Trong quá trình phát triển ứng dụng Flutter, State Management là một trong những vấn đề quan trọng cần được giải quyết để đảm bảo dữ liệu được quản lý nhất quán và giao diện được cập nhật chính xác. Khi ứng dụng chỉ có một vài màn hình, lập trình viên có thể sử dụng setState() để thay đổi trạng thái. Tuy nhiên, khi ứng dụng mở rộng với nhiều màn hình, các chức năng dùng chung, yêu cầu API và quy trình nghiệp vụ phức tạp, việc quản lý trạng thái trở nên khó khăn hơn.

Để giải quyết vấn đề này, Flutter có thể sử dụng nhiều giải pháp quản lý trạng thái khác nhau. Trong đó, Riverpod, BLoC và Provider là ba lựa chọn thường gặp trong quá trình xây dựng ứng dụng.

Provider là thư viện hỗ trợ cung cấp và chia sẻ dữ liệu giữa các Widget, thường kết hợp với ChangeNotifier để thông báo khi trạng thái thay đổi.

Riverpod là giải pháp quản lý trạng thái và phụ thuộc, cung cấp cơ chế khai báo các nguồn dữ liệu, theo dõi thay đổi và xử lý dữ liệu bất đồng bộ.

BLoC, viết tắt của Business Logic Component, là mô hình tổ chức logic nghiệp vụ theo hướng tiếp nhận sự kiện, xử lý nghiệp vụ và phát ra trạng thái mới. Trong hệ sinh thái Flutter, mô hình này thường được triển khai bằng thư viện bloc hoặc flutter_bloc.

Cả ba giải pháp đều hướng đến mục tiêu tách biệt giao diện với dữ liệu và logic xử lý. Tuy nhiên, chúng khác nhau về cách tổ chức mã nguồn, cách cập nhật trạng thái, cách quản lý phụ thuộc và mức độ phù hợp với từng loại ứng dụng.

Việc hiểu rõ đặc điểm của từng giải pháp giúp lập trình viên lựa chọn công nghệ phù hợp, hạn chế sự phụ thuộc không cần thiết và xây dựng cấu trúc ứng dụng dễ bảo trì.

2. Provider trong Flutter

2.1. Khái niệm Provider

Provider là một thư viện quản lý trạng thái phổ biến trong Flutter, hỗ trợ cung cấp đối tượng dữ liệu cho các Widget con trong cây giao diện. Thư viện giúp những thành phần cần sử dụng dữ liệu có thể truy cập đối tượng dùng chung mà không cần truyền dữ liệu qua từng cấp Widget bằng constructor.

Provider thường được sử dụng kết hợp với ChangeNotifier, một lớp có sẵn trong Flutter cho phép đối tượng thông báo đến các thành phần đang lắng nghe khi dữ liệu thay đổi.

Trong mô hình này, lập trình viên xây dựng một lớp chịu trách nhiệm quản lý trạng thái. Lớp đó lưu trữ dữ liệu, cung cấp các phương thức thực hiện thao tác và thông báo khi có thay đổi. Những Widget sử dụng Provider có thể lấy dữ liệu hiện tại hoặc đăng ký theo dõi để cập nhật giao diện.

Ví dụ, trong ứng dụng bán hàng, một lớp CartProvider có thể quản lý danh sách sản phẩm trong giỏ hàng. Khi người dùng thêm sản phẩm, lớp này cập nhật danh sách và thông báo cho các Widget liên quan. Biểu tượng giỏ hàng, danh sách sản phẩm và tổng số tiền có thể được cập nhật dựa trên dữ liệu mới.

Provider giúp tổ chức trạng thái theo hướng tập trung hơn so với việc để mỗi Widget tự quản lý một bản sao dữ liệu riêng biệt.

2.2. Các thành phần chính của Provider

ChangeNotifier

ChangeNotifier là cơ chế thông báo thay đổi được tích hợp trong Flutter. Một lớp có thể kế thừa ChangeNotifier để thông báo cho những thành phần đang lắng nghe khi dữ liệu của nó thay đổi.

Phương thức thường gặp gồm:

  • notifyListeners(): Thông báo cho các listener rằng dữ liệu đã thay đổi.
  • addListener(): Đăng ký một listener để nhận thông báo.
  • removeListener(): Hủy đăng ký listener.
  • dispose(): Giải phóng tài nguyên và loại bỏ những listener thuộc đối tượng khi đối tượng không còn được sử dụng.

Trong ứng dụng thực tế, lập trình viên thường không cần tự quản lý listener trực tiếp nếu đã sử dụng Provider đúng cách.

ChangeNotifierProvider

ChangeNotifierProvider là thành phần dùng để cung cấp một đối tượng ChangeNotifier cho các Widget con. Nó hỗ trợ quản lý vòng đời đối tượng theo cách sử dụng tương ứng và cho phép các Widget truy cập đối tượng được cung cấp.

Ví dụ, ChangeNotifierProvider có thể cung cấp một đối tượng quản lý giỏ hàng cho toàn bộ màn hình mua sắm.

context.watch()

context.watch() được sử dụng để truy cập một đối tượng được cung cấp và đăng ký theo dõi thay đổi của đối tượng đó. Khi đối tượng thông báo thay đổi, Widget đang theo dõi có thể được xây dựng lại.

Phương pháp này phù hợp khi giao diện cần phản ánh dữ liệu mới, chẳng hạn như số lượng sản phẩm hoặc tổng tiền trong giỏ hàng.

context.read()

context.read() dùng để lấy đối tượng mà không đăng ký theo dõi các thay đổi của đối tượng đó.

Phương pháp này thường được sử dụng trong các thao tác như nhấn nút thêm sản phẩm, đăng xuất hoặc gửi yêu cầu cập nhật dữ liệu.

Consumer

Consumer cho phép một khu vực cụ thể của giao diện sử dụng dữ liệu từ Provider. Việc đặt Consumer tại vị trí phù hợp có thể giúp giới hạn phạm vi Widget được xây dựng lại khi trạng thái thay đổi.

2.3. Cơ chế hoạt động của Provider

Quy trình hoạt động cơ bản của Provider gồm các bước:

  1. Khởi tạo một đối tượng quản lý trạng thái.
  2. Cung cấp đối tượng đó cho cây Widget thông qua Provider.
  3. Widget truy cập dữ liệu hoặc gọi phương thức xử lý.
  4. Đối tượng quản lý trạng thái cập nhật dữ liệu.
  5. Đối tượng gọi notifyListeners() để thông báo thay đổi.
  6. Những Widget đang theo dõi nhận thông báo và cập nhật giao diện.

Ví dụ, khi người dùng nhấn nút tăng số lượng sản phẩm, phương thức xử lý sẽ thay đổi giá trị bộ đếm. Sau đó, notifyListeners() thông báo cho các thành phần liên quan để chúng hiển thị số lượng mới.

Cơ chế này giúp tách nơi quản lý dữ liệu khỏi nơi hiển thị dữ liệu, từ đó giảm sự phụ thuộc giữa các Widget.

2.4. Ví dụ triển khai Provider

Trước tiên, cài đặt thư viện bằng lệnh:

flutter pub add provider

Tạo lớp quản lý trạng thái CounterProvider:

import 'package:flutter/foundation.dart';

class CounterProvider extends ChangeNotifier {
  int _counter = 0;

  int get counter => _counter;

  void increase() {
    _counter++;
    notifyListeners();
  }

  void decrease() {
    if (_counter > 0) {
      _counter--;
      notifyListeners();
    }
  }

  void reset() {
    _counter = 0;
    notifyListeners();
  }
}

Trong lớp này, biến _counter lưu trữ trạng thái của bộ đếm. Getter counter cho phép bên ngoài đọc giá trị nhưng không trực tiếp thay đổi biến nội bộ.

Phương thức increase() tăng giá trị lên một đơn vị, decrease() giảm giá trị nếu lớn hơn 0 và reset() đưa giá trị về 0. Sau mỗi lần thay đổi, notifyListeners() được gọi để thông báo trạng thái mới.

Tiếp theo, sử dụng Provider trong giao diện:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterProvider(),
      child: const MaterialApp(
        home: CounterPage(),
      ),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    final counter = context.watch<CounterProvider>();

    return Scaffold(
      appBar: AppBar(
        title: const Text('Provider Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Giá trị: ${counter.counter}',
              style: const TextStyle(fontSize: 24),
            ),
            ElevatedButton(
              onPressed: () {
                context.read<CounterProvider>().increase();
              },
              child: const Text('Tăng'),
            ),
            ElevatedButton(
              onPressed: () {
                context.read<CounterProvider>().decrease();
              },
              child: const Text('Giảm'),
            ),
            ElevatedButton(
              onPressed: () {
                context.read<CounterProvider>().reset();
              },
              child: const Text('Đặt lại'),
            ),
          ],
        ),
      ),
    );
  }
}

Trong ví dụ trên, ChangeNotifierProvider cung cấp đối tượng CounterProvider. Widget sử dụng context.watch() để theo dõi thay đổi của đối tượng. Các nút sử dụng context.read() để gọi phương thức mà không tạo thêm đăng ký theo dõi.

Khi người dùng nhấn nút, giá trị bộ đếm được cập nhật và giao diện hiển thị kết quả mới.

2.5. Ưu điểm và hạn chế của Provider

Provider có cú pháp tương đối dễ tiếp cận, phù hợp với người mới học Flutter và có thể triển khai nhiều chức năng quản lý trạng thái mà không cần xây dựng quá nhiều lớp.

Thư viện cũng hỗ trợ chia sẻ đối tượng giữa các màn hình, giảm việc truyền dữ liệu thủ công và có thể kết hợp với các mô hình kiến trúc khác.

Tuy nhiên, nếu một lớp ChangeNotifier quản lý quá nhiều dữ liệu và nghiệp vụ, việc xác định nguyên nhân của một thay đổi có thể trở nên khó khăn. Ngoài ra, gọi notifyListeners() không hợp lý có thể khiến các Widget không cần thiết phải xây dựng lại.

Provider phù hợp với những ứng dụng cần cơ chế quản lý trạng thái dễ hiểu, cấu trúc vừa phải và có thể mở rộng theo nhu cầu.

3. Riverpod trong Flutter

3.1. Khái niệm Riverpod

Riverpod là một giải pháp quản lý trạng thái và phụ thuộc dành cho Dart và Flutter. Riverpod phát triển từ những ý tưởng tương tự Provider nhưng có cách tổ chức riêng, hỗ trợ khai báo các nguồn dữ liệu độc lập với vị trí của Widget trong cây giao diện.

Trong Riverpod, các đối tượng được gọi là Provider đóng vai trò cung cấp dữ liệu, dịch vụ hoặc trạng thái cho những thành phần cần sử dụng. Widget có thể theo dõi các Provider để nhận dữ liệu mới khi trạng thái thay đổi.

Riverpod hỗ trợ nhiều loại dữ liệu, từ giá trị đơn giản đến dữ liệu bất đồng bộ được lấy từ API. Nhờ đó, lập trình viên có thể sử dụng một hệ thống thống nhất để quản lý dữ liệu, trạng thái tải, kết quả và lỗi.

Một điểm quan trọng là Riverpod có thể được sử dụng để quản lý logic bên ngoài Widget. Điều này giúp mã nguồn dễ kiểm thử hơn và hạn chế việc đặt toàn bộ xử lý nghiệp vụ trong giao diện.

3.2. Các thành phần chính của Riverpod

Provider

Provider cung cấp một giá trị hoặc đối tượng để những thành phần khác truy cập. Nó có thể dùng để cung cấp cấu hình, dịch vụ, Repository hoặc dữ liệu được tính toán từ những nguồn khác.

ProviderScope

ProviderScope là thành phần thường được đặt ở cấp gốc của ứng dụng Flutter để thiết lập môi trường sử dụng Riverpod. Nó quản lý vùng chứa trạng thái của các Provider trong phạm vi tương ứng.

Ví dụ, ProviderScope thường được đặt bên ngoài MaterialApp để những màn hình bên trong có thể sử dụng các Provider đã khai báo.

ConsumerWidget

ConsumerWidget là dạng Widget cho phép truy cập WidgetRef trong phương thức build(). Thông qua ref, Widget có thể đọc hoặc theo dõi dữ liệu được cung cấp bởi Riverpod.

ref.watch()

ref.watch() dùng để theo dõi một Provider. Khi giá trị của Provider thay đổi, thành phần đang theo dõi sẽ được cập nhật theo cơ chế của Riverpod.

Phương thức này phù hợp khi giao diện cần hiển thị dữ liệu luôn phản ánh trạng thái hiện tại.

ref.read()

ref.read() dùng để đọc giá trị mà không đăng ký theo dõi các thay đổi. Nó thường được sử dụng khi gọi một phương thức xử lý, chẳng hạn như gửi yêu cầu đăng nhập hoặc thêm sản phẩm vào giỏ hàng.

ref.listen()

ref.listen() cho phép phản ứng với sự thay đổi của Provider bằng một callback. Nó hữu ích đối với những tác vụ như hiển thị thông báo, điều hướng hoặc thực hiện một hành động phụ khi trạng thái thay đổi.

Việc sử dụng ref.listen() cần tuân theo vị trí và quy tắc của API tương ứng, chẳng hạn sử dụng trong build() của ConsumerWidget hoặc trong những ngữ cảnh được Riverpod hỗ trợ.

Notifier và NotifierProvider

Notifier là cơ chế xây dựng lớp quản lý trạng thái trong Riverpod. Lớp này định nghĩa trạng thái ban đầu và các phương thức thay đổi trạng thái.

NotifierProvider cung cấp đối tượng Notifier và trạng thái mà nó quản lý. Đây là cách tiếp cận phù hợp khi ứng dụng có những trạng thái cần được cập nhật thông qua các thao tác nghiệp vụ.

FutureProvider và StreamProvider

FutureProvider phù hợp với những thao tác bất đồng bộ trả về một kết quả, chẳng hạn tải thông tin người dùng hoặc danh sách sản phẩm.

StreamProvider phù hợp với dữ liệu được phát liên tục theo thời gian, chẳng hạn những thay đổi từ một Stream hoặc nguồn dữ liệu cập nhật liên tục.

Các Provider bất đồng bộ giúp ứng dụng xử lý trạng thái chờ, kết quả và lỗi theo cách có cấu trúc.

3.3. Cơ chế hoạt động của Riverpod

Quy trình hoạt động cơ bản của Riverpod có thể được mô tả như sau:

  1. Khai báo Provider hoặc Notifier để xác định nguồn dữ liệu và logic quản lý trạng thái.
  2. Thiết lập ProviderScope cho ứng dụng.
  3. Widget sử dụng ref.watch() để theo dõi dữ liệu cần thiết.
  4. Người dùng thực hiện thao tác và Widget gọi phương thức tương ứng thông qua ref.read().
  5. Thành phần quản lý trạng thái cập nhật dữ liệu.
  6. Riverpod thông báo những thay đổi liên quan đến các thành phần đang theo dõi.
  7. Giao diện được cập nhật theo giá trị mới.

Đối với dữ liệu bất đồng bộ, Riverpod còn hỗ trợ biểu diễn trạng thái đang tải, thành công và thất bại. Điều này giúp giao diện xử lý các trường hợp khác nhau một cách rõ ràng.

3.4. Ví dụ triển khai Riverpod

Cài đặt thư viện:

flutter pub add flutter_riverpod

Ví dụ dưới đây sử dụng NotifierProvider để quản lý bộ đếm:

import 'package:flutter_riverpod/flutter_riverpod.dart';

final counterProvider =
    NotifierProvider<CounterNotifier, int>(
  CounterNotifier.new,
);

class CounterNotifier extends Notifier<int> {
  @override
  int build() {
    return 0;
  }

  void increase() {
    state++;
  }

  void decrease() {
    if (state > 0) {
      state--;
    }
  }

  void reset() {
    state = 0;
  }
}

Trong ví dụ này, counterProvider cung cấp trạng thái kiểu int. Lớp CounterNotifier kế thừa Notifier<int> và định nghĩa giá trị ban đầu thông qua phương thức build().

Biến state đại diện cho trạng thái mà Notifier quản lý. Khi state thay đổi, Riverpod thông báo đến những thành phần đang theo dõi Provider này.

Tiếp theo, xây dựng giao diện:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'counter_provider.dart';

void main() {
  runApp(
    const ProviderScope(
      child: MaterialApp(
        home: CounterPage(),
      ),
    ),
  );
}

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

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

    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Giá trị: $counter',
              style: const TextStyle(fontSize: 24),
            ),
            ElevatedButton(
              onPressed: () {
                ref.read(counterProvider.notifier).increase();
              },
              child: const Text('Tăng'),
            ),
            ElevatedButton(
              onPressed: () {
                ref.read(counterProvider.notifier).decrease();
              },
              child: const Text('Giảm'),
            ),
            ElevatedButton(
              onPressed: () {
                ref.read(counterProvider.notifier).reset();
              },
              child: const Text('Đặt lại'),
            ),
          ],
        ),
      ),
    );
  }
}

Trong đoạn mã trên, ProviderScope thiết lập môi trường Riverpod. ConsumerWidget cung cấp đối tượng WidgetRef để truy cập Provider.

ref.watch(counterProvider) theo dõi trạng thái bộ đếm. Khi trạng thái thay đổi, giao diện được cập nhật. Trong khi đó, ref.read(counterProvider.notifier) cho phép gọi các phương thức thay đổi trạng thái mà không đăng ký theo dõi từ chính thao tác đó.

3.5. Quản lý dữ liệu bất đồng bộ với Riverpod

Một trong những tình huống phổ biến trong Flutter là tải dữ liệu từ API. Quá trình này cần thời gian chờ và có thể xảy ra lỗi, do đó ứng dụng cần quản lý nhiều trạng thái khác nhau.

Ví dụ, sử dụng FutureProvider để tải danh sách sản phẩm:

import 'package:flutter_riverpod/flutter_riverpod.dart';

class Product {
  final int id;
  final String name;

  const Product({
    required this.id,
    required this.name,
  });
}

final productsProvider = FutureProvider<List<Product>>((ref) async {
  await Future.delayed(const Duration(seconds: 1));

  return const [
    Product(id: 1, name: 'Laptop'),
    Product(id: 2, name: 'Điện thoại'),
    Product(id: 3, name: 'Tai nghe'),
  ];
});

Ví dụ này mô phỏng quá trình tải dữ liệu bằng một khoảng thời gian chờ. Trong ứng dụng thực tế, phần thân của Provider có thể gọi API thông qua một Repository.

Widget hiển thị dữ liệu:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'products_provider.dart';

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

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

    return Scaffold(
      appBar: AppBar(
        title: const Text('Danh sách sản phẩm'),
      ),
      body: products.when(
        loading: () => const Center(
          child: CircularProgressIndicator(),
        ),
        error: (error, stackTrace) => Center(
          child: Text('Đã xảy ra lỗi: $error'),
        ),
        data: (items) => ListView.builder(
          itemCount: items.length,
          itemBuilder: (context, index) {
            final product = items[index];

            return ListTile(
              title: Text(product.name),
              subtitle: Text('Mã sản phẩm: ${product.id}'),
            );
          },
        ),
      ),
    );
  }
}

Phương thức when() giúp xử lý ba trường hợp chính: đang tải, xảy ra lỗi và tải thành công. Khi dữ liệu được trả về, ứng dụng xây dựng danh sách sản phẩm. Nếu có lỗi, giao diện hiển thị thông báo thay vì bị trống.

Ví dụ này minh họa cách tổ chức trạng thái bất đồng bộ; nó chưa thực hiện yêu cầu HTTP thực tế.

3.6. Ưu điểm và hạn chế của Riverpod

Riverpod hỗ trợ quản lý trạng thái theo hướng khai báo, cho phép các thành phần theo dõi dữ liệu cần thiết và tách logic khỏi Widget. Thư viện cũng cung cấp nhiều công cụ cho dữ liệu bất đồng bộ, quản lý phụ thuộc và kiểm thử.

Việc tổ chức Provider riêng biệt giúp lập trình viên xác định nguồn dữ liệu rõ ràng hơn, đặc biệt trong ứng dụng có nhiều chức năng.

Tuy nhiên, Riverpod có nhiều khái niệm và API khác nhau. Người mới học có thể cần thời gian để hiểu cách sử dụng Provider, Notifier, FutureProvider, ref.watch() và các cơ chế liên quan.

Riverpod phù hợp với những dự án cần quản lý phụ thuộc rõ ràng, xử lý dữ liệu bất đồng bộ thường xuyên và có yêu cầu kiểm thử độc lập.

4. BLoC trong Flutter

4.1. Khái niệm BLoC

BLoC là viết tắt của Business Logic Component, một mô hình tổ chức logic nghiệp vụ theo hướng tách biệt với giao diện. Trong Flutter, thư viện bloc và flutter_bloc được sử dụng phổ biến để triển khai mô hình này.

Mục tiêu chính của BLoC là xác định rõ cách dữ liệu đi từ giao diện đến logic nghiệp vụ và cách kết quả được trả về giao diện.

Trong mô hình BLoC truyền thống, giao diện gửi một Event để mô tả hành động vừa xảy ra. BLoC tiếp nhận Event, thực hiện xử lý và phát ra một State mới. Giao diện theo dõi State để cập nhật nội dung hiển thị.

Ví dụ, với chức năng đăng nhập, người dùng nhập tên tài khoản và mật khẩu rồi nhấn nút đăng nhập. Giao diện gửi sự kiện LoginSubmitted đến BLoC. BLoC kiểm tra dữ liệu, gọi dịch vụ xác thực và phát ra trạng thái đang đăng nhập, đăng nhập thành công hoặc đăng nhập thất bại.

Nhờ luồng xử lý có cấu trúc, BLoC giúp phân tách trách nhiệm giữa giao diện, sự kiện, nghiệp vụ và kết quả xử lý.

4.2. Các thành phần chính của BLoC

Event

Event đại diện cho một sự kiện hoặc yêu cầu cần được xử lý. Sự kiện thường xuất phát từ hành động của người dùng hoặc một hoạt động trong hệ thống.

Ví dụ:

  • CounterIncremented: Yêu cầu tăng bộ đếm.
  • CounterDecremented: Yêu cầu giảm bộ đếm.
  • LoginSubmitted: Gửi thông tin đăng nhập.
  • ProductsRequested: Yêu cầu tải danh sách sản phẩm.

Event mô tả điều gì đã xảy ra hoặc thao tác nào cần thực hiện, thay vì trực tiếp đảm nhận việc thay đổi giao diện.

State

State biểu diễn trạng thái hiện tại mà giao diện cần phản ánh. Với bộ đếm, State có thể chứa một số nguyên. Với chức năng đăng nhập, State có thể thể hiện trạng thái ban đầu, đang xử lý, thành công hoặc thất bại.

State nên được thiết kế sao cho thể hiện rõ thông tin cần thiết, tránh chứa quá nhiều dữ liệu không liên quan.

Bloc

Bloc là thành phần tiếp nhận Event, xử lý logic và phát ra State mới. Bloc có thể gọi Repository hoặc các dịch vụ bên ngoài khi nghiệp vụ yêu cầu.

Luồng xử lý của Bloc thường được tổ chức thông qua các Event Handler. Mỗi handler chịu trách nhiệm xử lý một nhóm sự kiện phù hợp và phát ra các trạng thái tương ứng.

Cubit

Cubit là một thành phần khác trong hệ sinh thái bloc, cho phép thay đổi trạng thái thông qua các phương thức thay vì sử dụng Event riêng biệt.

Ví dụ, CounterCubit có thể cung cấp phương thức increment() để tăng bộ đếm và phát ra giá trị mới.

Cubit thường phù hợp với những nghiệp vụ đơn giản hoặc những trường hợp không cần mô hình hóa đầu vào thành các lớp Event riêng.

BlocProvider

BlocProvider là Widget dùng để cung cấp một Bloc hoặc Cubit cho các Widget con. Nó hỗ trợ quản lý vòng đời đối tượng theo cách khởi tạo được sử dụng.

BlocBuilder

BlocBuilder xây dựng giao diện dựa trên State hiện tại. Khi trạng thái thay đổi, Widget có thể được xây dựng lại để hiển thị dữ liệu mới.

BlocListener

BlocListener dùng để thực hiện các hành động phụ khi State thay đổi, chẳng hạn hiển thị thông báo, điều hướng sang màn hình khác hoặc mở hộp thoại.

Thông thường, BlocListener không trực tiếp đảm nhiệm việc xây dựng giao diện dựa trên State như BlocBuilder.

BlocConsumer

BlocConsumer kết hợp khả năng xây dựng giao diện của BlocBuilder và thực hiện hành động phụ của BlocListener. Nó phù hợp khi cùng một khu vực cần phản ánh trạng thái mới và thực hiện một phản ứng bổ sung.

4.3. Cơ chế hoạt động của BLoC

Luồng xử lý cơ bản của BLoC gồm các bước:

  1. Người dùng thực hiện hành động trên giao diện.
  2. Widget tạo và gửi Event đến Bloc.
  3. Bloc tiếp nhận Event tương ứng.
  4. Logic nghiệp vụ được thực thi, có thể bao gồm gọi API hoặc Repository.
  5. Bloc phát ra State mới.
  6. Những Widget đang theo dõi State nhận được cập nhật.
  7. Giao diện hiển thị kết quả phù hợp.

Có thể biểu diễn luồng này như sau:

UI → Event → BLoC → State → UI.

Ví dụ, khi người dùng yêu cầu tải danh sách sản phẩm, giao diện gửi ProductsRequested. Bloc phát ra trạng thái ProductsLoading, gọi Repository để lấy dữ liệu, sau đó phát ra ProductsLoaded nếu thành công hoặc ProductsFailure nếu thất bại.

Việc thể hiện rõ từng trạng thái giúp nhóm phát triển dễ theo dõi quy trình xử lý và xây dựng kiểm thử cho các trường hợp khác nhau.

4.4. Ví dụ triển khai BLoC

Cài đặt thư viện:

flutter pub add flutter_bloc

Tạo các trạng thái:

abstract class CounterState {
  final int value;

  const CounterState(this.value);
}

class CounterInitial extends CounterState {
  const CounterInitial() : super(0);
}

class CounterUpdated extends CounterState {
  const CounterUpdated(super.value);
}

Trong ví dụ này, CounterState là lớp cơ sở biểu diễn trạng thái bộ đếm. Hai lớp con thể hiện trạng thái ban đầu và trạng thái đã được cập nhật.

Tiếp theo, tạo các sự kiện:

abstract class CounterEvent {
  const CounterEvent();
}

class CounterIncremented extends CounterEvent {
  const CounterIncremented();
}

class CounterDecremented extends CounterEvent {
  const CounterDecremented();
}

class CounterReset extends CounterEvent {
  const CounterReset();
}

Mỗi lớp Event biểu diễn một thao tác cụ thể. Cách tổ chức này giúp những hành động khác nhau được nhận diện và xử lý rõ ràng.

Tạo Bloc:

import 'package:flutter_bloc/flutter_bloc.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterInitial()) {
    on<CounterIncremented>((event, emit) {
      emit(CounterUpdated(state.value + 1));
    });

    on<CounterDecremented>((event, emit) {
      if (state.value > 0) {
        emit(CounterUpdated(state.value - 1));
      }
    });

    on<CounterReset>((event, emit) {
      emit(const CounterUpdated(0));
    });
  }
}

CounterBloc kế thừa Bloc<CounterEvent, CounterState>. Tham số đầu tiên xác định loại Event được tiếp nhận, còn tham số thứ hai xác định loại State được phát ra.

Các phương thức on<T>() đăng ký cách xử lý từng loại Event. Khi sự kiện tăng bộ đếm được gửi đến Bloc, handler tính toán giá trị mới và gọi emit() để phát ra trạng thái tương ứng.

Xây dựng giao diện:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(
    MaterialApp(
      home: BlocProvider(
        create: (_) => CounterBloc(),
        child: const CounterPage(),
      ),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('BLoC Example'),
      ),
      body: Center(
        child: BlocBuilder<CounterBloc, CounterState>(
          builder: (context, state) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'Giá trị: ${state.value}',
                  style: const TextStyle(fontSize: 24),
                ),
                ElevatedButton(
                  onPressed: () {
                    context.read<CounterBloc>().add(
                      const CounterIncremented(),
                    );
                  },
                  child: const Text('Tăng'),
                ),
                ElevatedButton(
                  onPressed: () {
                    context.read<CounterBloc>().add(
                      const CounterDecremented(),
                    );
                  },
                  child: const Text('Giảm'),
                ),
                ElevatedButton(
                  onPressed: () {
                    context.read<CounterBloc>().add(
                      const CounterReset(),
                    );
                  },
                  child: const Text('Đặt lại'),
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

Trong ví dụ này, BlocProvider cung cấp đối tượng CounterBloc cho màn hình. BlocBuilder theo dõi trạng thái và xây dựng giao diện dựa trên giá trị hiện tại.

Khi người dùng nhấn nút, giao diện gửi Event thông qua phương thức add(). Bloc xử lý sự kiện và phát ra State mới. Sau đó, BlocBuilder nhận trạng thái mới và cập nhật giao diện.

Ví dụ sử dụng các lớp State và Event riêng để minh họa mô hình BLoC. Trong dự án thực tế, có thể cần bổ sung các trường dữ liệu, xử lý lỗi và cấu trúc trạng thái phù hợp với nghiệp vụ.

4.5. Cubit và BLoC

Cubit và BLoC đều thuộc hệ sinh thái bloc, nhưng cách tiếp cận đầu vào khác nhau.

Tiêu chí Cubit BLoC
Đầu vào Phương thức được gọi trực tiếp Event được gửi vào Bloc
Cách thay đổi State Phương thức gọi emit() Event Handler gọi emit()
Số lượng lớp Thường ít hơn Có thể cần thêm lớp Event
Khả năng thể hiện luồng sự kiện Đơn giản, trực tiếp Tách biệt từng loại sự kiện
Phù hợp Bộ đếm, bộ lọc, trạng thái đơn giản Luồng nghiệp vụ có nhiều loại sự kiện

Ví dụ với Cubit:

import 'package:flutter_bloc/flutter_bloc.dart';

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() {
    emit(state + 1);
  }

  void decrement() {
    if (state > 0) {
      emit(state - 1);
    }
  }

  void reset() {
    emit(0);
  }
}

Cubit quản lý trực tiếp trạng thái kiểu int. Khi gọi increment(), Cubit phát ra giá trị mới bằng emit(state + 1).

Đối với bộ đếm, Cubit có thể thực hiện cùng chức năng với BLoC nhưng ít mã nguồn hơn. Khi ứng dụng cần theo dõi nhiều loại sự kiện, ghi nhận lịch sử sự kiện hoặc tổ chức luồng xử lý riêng biệt, BLoC có thể cung cấp cấu trúc thể hiện rõ ràng hơn.

4.6. Ưu điểm và hạn chế của BLoC

BLoC giúp phân tách rõ giao diện, đầu vào, logic nghiệp vụ và kết quả. Việc tổ chức các trạng thái thành những kiểu dữ liệu riêng hỗ trợ kiểm thử và giúp lập trình viên xác định ứng dụng đang ở giai đoạn nào của một quy trình.

Mô hình này phù hợp với những chức năng có nhiều bước xử lý như đăng nhập, thanh toán, quản lý đơn hàng hoặc tải dữ liệu từ nhiều nguồn.

Tuy nhiên, BLoC có thể yêu cầu nhiều lớp và mã nguồn hơn so với các giải pháp đơn giản. Nếu sử dụng cho một chức năng quá nhỏ, việc tạo Event, State và Bloc riêng có thể làm tăng độ phức tạp không cần thiết.

Để sử dụng BLoC hiệu quả, cần thiết kế trạng thái rõ ràng, tránh tạo quá nhiều State trùng lặp và không đưa toàn bộ nghiệp vụ của ứng dụng vào một Bloc duy nhất.

5. So sánh Provider, Riverpod và BLoC

Provider, Riverpod và BLoC đều hỗ trợ tách biệt trạng thái khỏi giao diện, nhưng cách tổ chức và xử lý dữ liệu có những khác biệt đáng chú ý.

5.1. So sánh về cơ chế hoạt động

Tiêu chí Provider Riverpod BLoC
Cách tiếp cận Cung cấp đối tượng, thường kết hợp ChangeNotifier Khai báo Provider và theo dõi phụ thuộc Xử lý Event và phát State; Cubit dùng phương thức
Cập nhật trạng thái Thường gọi notifyListeners() với ChangeNotifier Cập nhật state qua Notifier hoặc cơ chế tương ứng Phát State bằng emit()
Truy cập dữ liệu context.watch(), context.read() ref.watch(), ref.read() BlocBuilder, BlocListener, context.read()
Dữ liệu bất đồng bộ Có thể kết hợp Future, Stream và các lớp xử lý Có FutureProvider, StreamProvider và các cơ chế tương ứng Xây dựng State cho quá trình tải, thành công và thất bại
Tổ chức nghiệp vụ Linh hoạt, phụ thuộc cách thiết kế lớp Linh hoạt, hỗ trợ tổ chức phụ thuộc Luồng xử lý được thể hiện rõ qua Event và State
Độ phức tạp ban đầu Thường thấp đến trung bình Trung bình Trung bình đến cao nếu dùng đầy đủ Event và State

Bảng trên thể hiện cách sử dụng phổ biến, không phải giới hạn bắt buộc của từng giải pháp. Cả ba đều có thể được kết hợp với Repository, Use Case hoặc những kiến trúc khác để phục vụ nghiệp vụ phức tạp.

5.2. So sánh về cấu trúc mã nguồn

Provider thường phù hợp với cấu trúc trong đó một lớp quản lý dữ liệu và cung cấp các phương thức cập nhật. Widget sử dụng Provider để đọc dữ liệu hoặc theo dõi các thay đổi.

Riverpod cho phép khai báo các nguồn dữ liệu và thành phần quản lý trạng thái riêng biệt. Những Provider có thể phụ thuộc vào nhau, giúp tổ chức dữ liệu và dịch vụ theo nhu cầu.

BLoC thường phân tách các thành phần thành Event, State và Bloc. Cấu trúc này tạo ra một luồng xử lý có thể quan sát rõ ràng, nhưng số lượng lớp có thể tăng lên khi nghiệp vụ phức tạp.

Việc lựa chọn cấu trúc nên dựa trên nhu cầu thực tế. Một dự án không nhất thiết phải chia mọi biến thành nhiều lớp chỉ để tuân thủ một mô hình quản lý trạng thái.

5.3. So sánh về khả năng kiểm thử

Cả ba giải pháp đều có thể được kiểm thử nếu logic được tổ chức phù hợp.

Provider có thể kiểm thử các phương thức trong lớp ChangeNotifier, sau đó kiểm tra thông báo và dữ liệu được cập nhật.

Riverpod cung cấp các cơ chế hỗ trợ kiểm thử Provider và quản lý những phụ thuộc cần thay thế trong môi trường thử nghiệm.

BLoC có thể được kiểm thử bằng cách cung cấp đầu vào là các Event hoặc lời gọi phương thức đối với Cubit, sau đó xác nhận chuỗi State được phát ra.

Trong những nghiệp vụ có nhiều trạng thái và nhiều bước xử lý, việc định nghĩa đầu vào và đầu ra rõ ràng giúp xây dựng các trường hợp kiểm thử có hệ thống.

5.4. So sánh về hiệu suất

Hiệu suất của ứng dụng không chỉ phụ thuộc vào thư viện quản lý trạng thái mà còn phụ thuộc vào cách thiết kế dữ liệu, phạm vi theo dõi và cấu trúc Widget.

Với Provider, lập trình viên cần chú ý phạm vi ảnh hưởng của notifyListeners() và vị trí sử dụng watch().

Với Riverpod, cần lựa chọn Provider phù hợp, theo dõi đúng dữ liệu cần thiết và tránh tạo những phụ thuộc không cần thiết.

Với BLoC, cần thiết kế State hợp lý và tránh phát ra quá nhiều trạng thái không cần thiết. Những Widget chỉ cần phản ứng với một số thay đổi có thể sử dụng các cơ chế chọn lọc dữ liệu phù hợp.

Do đó, không nên kết luận rằng một giải pháp luôn nhanh hơn hai giải pháp còn lại trong mọi tình huống. Hiệu suất cần được đánh giá thông qua cấu trúc triển khai và quá trình đo đạc thực tế.

6. Lựa chọn giải pháp theo quy mô dự án

6.1. Dự án nhỏ

Đối với ứng dụng có ít màn hình, ít dữ liệu dùng chung và logic đơn giản, Provider hoặc setState() có thể đáp ứng tốt nhu cầu.

Ví dụ, ứng dụng ghi chú cá nhân đơn giản có thể sử dụng một lớp quản lý danh sách ghi chú và cung cấp dữ liệu cho những Widget cần thiết.

Trong trường hợp này, việc sử dụng một mô hình quá phức tạp có thể làm tăng thời gian phát triển mà không đem lại lợi ích tương xứng.

6.2. Dự án có nhiều dữ liệu và phụ thuộc

Đối với ứng dụng có nhiều nguồn dữ liệu, cần gọi API thường xuyên và có những dịch vụ phụ thuộc lẫn nhau, Riverpod có thể giúp tổ chức các nguồn dữ liệu theo cách khai báo rõ ràng.

Ví dụ, một ứng dụng thương mại điện tử có thể có Provider cho thông tin người dùng, danh sách sản phẩm, bộ lọc tìm kiếm và dữ liệu giỏ hàng. Những thành phần này có thể được tổ chức riêng, sau đó kết hợp khi xây dựng các màn hình.

6.3. Dự án có quy trình nghiệp vụ phức tạp

Đối với ứng dụng có nhiều quy trình cần theo dõi rõ ràng, BLoC có thể phù hợp nhờ cách biểu diễn Event và State.

Ví dụ, quy trình thanh toán có thể bao gồm xác nhận giỏ hàng, kiểm tra thông tin giao hàng, gửi yêu cầu thanh toán và xử lý kết quả. Mỗi giai đoạn có thể được thể hiện bằng một trạng thái cụ thể, giúp giao diện hiển thị đúng nội dung và phản hồi phù hợp.

Tuy nhiên, không phải mọi nghiệp vụ phức tạp đều bắt buộc phải sử dụng BLoC. Riverpod hoặc Provider vẫn có thể được sử dụng nếu cấu trúc logic được thiết kế phù hợp.

6.4. Dự án làm việc theo nhóm

Trong dự án có nhiều thành viên, điều quan trọng không chỉ là lựa chọn thư viện mà còn là thống nhất cách tổ chức mã nguồn.

Nhóm phát triển cần xác định rõ:

  • Nơi lưu trữ trạng thái.
  • Quy tắc đặt tên Provider, Bloc, Cubit, Event và State.
  • Cách gọi API và xử lý lỗi.
  • Cách phân chia trách nhiệm giữa giao diện và nghiệp vụ.
  • Cách kiểm thử các luồng xử lý.
  • Quy tắc quản lý vòng đời của đối tượng.

Một quy ước chung giúp các thành viên hiểu mã nguồn của nhau và hạn chế tình trạng mỗi chức năng được triển khai theo một cách hoàn toàn khác biệt.

7. Ứng dụng trong hệ thống thương mại điện tử

Đối với một ứng dụng thương mại điện tử được phát triển bằng Flutter, State Management có thể được sử dụng trong nhiều chức năng.

7.1. Quản lý danh sách sản phẩm

Ứng dụng cần tải dữ liệu sản phẩm từ API, hiển thị trạng thái đang tải, xử lý lỗi và cập nhật danh sách sau khi người dùng thay đổi bộ lọc.

Riverpod có thể sử dụng Provider bất đồng bộ để quản lý dữ liệu. BLoC có thể tiếp nhận sự kiện tải sản phẩm hoặc thay đổi bộ lọc rồi phát ra trạng thái tương ứng. Provider cũng có thể đảm nhiệm chức năng này nếu được kết hợp với các lớp xử lý dữ liệu phù hợp.

7.2. Quản lý giỏ hàng

Giỏ hàng thường bao gồm danh sách sản phẩm, số lượng, đơn giá và những thông tin cần thiết để tính tổng giá trị.

Một thành phần quản lý trạng thái có thể cung cấp các phương thức thêm sản phẩm, xóa sản phẩm, thay đổi số lượng và làm trống giỏ hàng.

Khi dữ liệu giỏ hàng thay đổi, những Widget cần thiết sẽ nhận thông tin mới. Tổng tiền có thể được tính toán dựa trên dữ liệu hiện tại, hạn chế việc lưu nhiều bản sao có thể không đồng nhất.

7.3. Quản lý xác thực người dùng

Trạng thái xác thực có thể bao gồm chưa đăng nhập, đang đăng nhập, đăng nhập thành công và đăng xuất.

Khi người dùng gửi thông tin đăng nhập, ứng dụng thực hiện yêu cầu xác thực và phản ánh kết quả thông qua State Management. Nếu thành công, giao diện có thể chuyển đến trang chủ. Nếu thất bại, ứng dụng hiển thị thông báo phù hợp.

Thông tin xác thực và quyền truy cập quan trọng vẫn phải được xử lý an toàn. State Management chỉ hỗ trợ tổ chức trạng thái ở phía ứng dụng, không thay thế việc xác thực và phân quyền trên máy chủ.

7.4. Quản lý đơn hàng

Trong chức năng đặt hàng, State Management giúp theo dõi quá trình kiểm tra dữ liệu, gửi yêu cầu tạo đơn hàng và nhận kết quả từ máy chủ.

Đối với quy trình nhiều bước, BLoC có thể biểu diễn các sự kiện và trạng thái tương ứng. Riverpod cũng có thể tổ chức trạng thái thông qua Notifier và những Provider phụ thuộc. Provider có thể được sử dụng nếu cấu trúc nghiệp vụ được tách biệt rõ ràng.

Việc quản lý trạng thái đúng cách giúp giao diện tránh gửi yêu cầu lặp lại không cần thiết, hiển thị tiến trình xử lý và phản ánh chính xác kết quả mà máy chủ trả về.

8. Những lưu ý khi triển khai Provider, Riverpod và BLoC

8.1. Không trộn lẫn nhiều giải pháp tùy tiện

Một dự án có thể sử dụng nhiều phương pháp quản lý trạng thái nếu có lý do hợp lý, chẳng hạn setState() cho trạng thái giao diện cục bộ và Riverpod cho dữ liệu dùng chung.

Tuy nhiên, sử dụng đồng thời nhiều thư viện lớn cho cùng một loại trạng thái có thể làm tăng độ phức tạp và khiến luồng dữ liệu khó theo dõi. Nhóm phát triển nên xác định giải pháp chính và quy định rõ phạm vi sử dụng của từng công cụ.

8.2. Không đặt toàn bộ logic vào lớp quản lý trạng thái

Provider, Notifier, Cubit hoặc Bloc không nên trở thành nơi chứa mọi logic của toàn bộ ứng dụng.

Khi nghiệp vụ phát triển, nên cân nhắc tách phần truy xuất dữ liệu sang Repository và tách những quy tắc nghiệp vụ phức tạp sang các thành phần phù hợp. Điều này giúp lớp quản lý trạng thái tập trung vào việc điều phối thao tác và cập nhật trạng thái.

8.3. Xử lý đầy đủ các tình huống lỗi

Đối với yêu cầu API, ứng dụng cần xem xét trạng thái tải, thành công, lỗi và dữ liệu rỗng. Với các thao tác ghi dữ liệu, cần xác định cách phản hồi khi máy chủ từ chối yêu cầu hoặc kết nối bị gián đoạn.

Những tình huống này nên được kiểm thử thay vì chỉ kiểm tra trường hợp thành công.

8.4. Quản lý vòng đời và tài nguyên

Các đối tượng quản lý trạng thái có thể sử dụng Stream, bộ điều khiển hoặc tài nguyên cần giải phóng. Việc đóng đối tượng đúng thời điểm giúp hạn chế rò rỉ tài nguyên và những cập nhật không còn cần thiết.

Cách xử lý vòng đời phụ thuộc vào thư viện và phương thức khởi tạo đối tượng. Lập trình viên cần tuân thủ hướng dẫn của công cụ đang sử dụng.

8.5. Sử dụng phiên bản thư viện phù hợp

Các thư viện quản lý trạng thái liên tục được cập nhật. Một số API, cách khai báo Provider hoặc phương thức khởi tạo có thể khác nhau giữa các phiên bản.

Khi triển khai dự án, cần kiểm tra phiên bản được khai báo trong pubspec.yaml, tài liệu tương ứng và các thay đổi của thư viện. Điều này giúp tránh lỗi do sử dụng cú pháp không tương thích với phiên bản hiện tại.

9. Kết luận

Provider, Riverpod và BLoC là những giải pháp quan trọng trong hệ sinh thái Flutter, giúp quản lý trạng thái, tổ chức dữ liệu và tách biệt giao diện khỏi logic nghiệp vụ. Mặc dù cùng hướng đến việc xây dựng ứng dụng dễ bảo trì và có khả năng mở rộng, mỗi giải pháp có cách tiếp cận riêng.

Provider cung cấp cơ chế chia sẻ đối tượng và theo dõi thay đổi, thường kết hợp với ChangeNotifier. Riverpod tổ chức trạng thái và các phụ thuộc theo hướng khai báo, hỗ trợ tốt những tình huống cần quản lý dữ liệu bất đồng bộ. BLoC tổ chức luồng xử lý thông qua Event và State, giúp thể hiện rõ quy trình nghiệp vụ; trong khi đó, Cubit cung cấp cách cập nhật trạng thái đơn giản hơn trong cùng hệ sinh thái.

Không có một giải pháp duy nhất phù hợp với mọi dự án. Việc lựa chọn cần dựa trên quy mô ứng dụng, mức độ phức tạp của nghiệp vụ, yêu cầu kiểm thử, kinh nghiệm của nhóm và khả năng bảo trì lâu dài.

Để sử dụng hiệu quả các công cụ này, lập trình viên cần hiểu bản chất của State Management, xác định phạm vi dữ liệu, tổ chức luồng cập nhật nhất quán và xử lý đầy đủ các trạng thái bất đồng bộ. Khi được áp dụng đúng cách, Provider, Riverpod và BLoC sẽ góp phần tạo nên những ứng dụng Flutter có cấu trúc rõ ràng, hoạt động ổn định và thuận lợi cho quá trình phát triển trong tương lai.

Bài viết khác

State Management

State Management 1. Tổng quan về State Management State Management là kỹ thuật quản lý trạng thái của ứng dụng, đóng vai trò quan trọng trong quá trình phát triển giao diện người dùng bằng Flutter. Kỹ thuật này giúp ứng dụng lưu trữ, cập nhật, chia sẻ và đồng bộ dữ liệu giữa các […]

Hive Local Storage trong Flutter

Hive Local Storage trong Flutter 1. Tổng quan về Hive Local Storage Hive là một cơ sở dữ liệu NoSQL dạng key-value được viết bằng Dart, thường được sử dụng trong các ứng dụng Flutter để lưu trữ dữ liệu trực tiếp trên thiết bị. Hive cho phép ứng dụng đọc, ghi, cập nhật và […]

Navigation & Routing

          Trong quá trình phát triển các ứng dụng web, ứng dụng di động và hệ thống bản đồ số, Navigation và Routing là hai khái niệm giữ vai trò trung tâm. Nếu Navigation tập trung vào cách người dùng di chuyển giữa các màn hình, trang hoặc chức năng, thì […]

Material & Cupertino

     Trong quá trình phát triển ứng dụng di động, giao diện người dùng không chỉ có nhiệm vụ trình bày thông tin mà còn phải tạo ra cảm giác quen thuộc, nhất quán và dễ sử dụng. Người dùng Android thường mong đợi các thành phần giao diện mang phong cách Material Design […]

Smoke Testing(Kiểm thử khói)

Smoke Testing là gì? Trong mô hình phát triển phần mềm hiện đại (dù là Agile, Scrum hay DevOps), tốc độ bàn giao các bản build (xây dựng mã nguồn) diễn ra vô cùng nhanh chóng. Mỗi ngày, đội ngũ lập trình viên có thể chuyển giao từ vài bản build nhỏ đến hàng chục […]

Flutter Layout & Responsive UI

      Trong phát triển ứng dụng hiện đại, giao diện người dùng không chỉ cần đẹp mà còn phải thích ứng tốt với nhiều kích thước màn hình, tỷ lệ khung hình và phương thức tương tác khác nhau. Một ứng dụng có thể được sử dụng trên điện thoại nhỏ, máy tính […]

Leave a Reply

Your email address will not be published. Required fields are marked *