Flutter là bộ công cụ phát triển giao diện người dùng do Google xây dựng, cho phép lập trình viên tạo ứng dụng đa nền tảng từ một cơ sở mã nguồn duy nhất. Điểm đặc biệt nhất của Flutter nằm ở triết lý mọi thứ đều là widget. Từ một dòng chữ, nút bấm, khoảng đệm, bố cục, màn hình cho đến toàn bộ ứng dụng đều được biểu diễn bằng widget.

Cách tiếp cận này giúp giao diện Flutter có tính nhất quán, khả năng tái sử dụng cao và dễ tổ chức theo cấu trúc phân cấp. Tuy nhiên, để khai thác tốt Flutter, người học cần hiểu widget không chỉ là một “thành phần giao diện”, mà còn là bản mô tả bất biến về cách một phần giao diện nên được xây dựng tại một thời điểm cụ thể. Bài luận này trình bày các khái niệm nền tảng của Flutter Widget, bao gồm widget tree, StatelessWidget, StatefulWidget, cơ chế build, hệ thống layout, quản lý trạng thái, key và các nguyên tắc thiết kế widget hiệu quả.

1. Widget là gì?

Trong Flutter, widget là một đối tượng mô tả một phần của giao diện người dùng. Widget không trực tiếp vẽ mọi thứ lên màn hình theo cách một thành phần UI truyền thống thường làm. Thay vào đó, nó cung cấp cấu hình để Flutter xác định cần tạo hoặc cập nhật giao diện như thế nào.

Ví dụ, đoạn mã sau tạo ra một giao diện đơn giản:

Center(
  child: Text(
    'Xin chào Flutter',
    style: TextStyle(fontSize: 24),
  ),
)

Trong ví dụ này, Center mô tả cách căn giữa phần tử con, còn Text mô tả nội dung văn bản và kiểu hiển thị. TextStyle cũng là một đối tượng được dùng để cấu hình giao diện. Có thể thấy, ngay cả các chi tiết nhỏ như căn chỉnh và kiểu chữ cũng được biểu diễn theo cách có cấu trúc.

Một đặc điểm quan trọng của widget là tính bất biến. Sau khi một widget được tạo, các thuộc tính của nó không nên bị thay đổi. Khi dữ liệu thay đổi, Flutter thường tạo ra một widget mới với cấu hình mới và so sánh nó với cấu hình cũ để cập nhật phần giao diện cần thiết.

2. Widget tree và mô hình phân cấp giao diện

Các widget trong Flutter được tổ chức thành một cấu trúc cây gọi là widget tree. Mỗi widget có thể chứa một widget con hoặc nhiều widget con, từ đó hình thành một giao diện phân cấp.

Ví dụ:

MaterialApp
└── Scaffold
    ├── AppBar
    └── Center
        └── Column
            ├── Text
            └── ElevatedButton

Cấu trúc cây này mang lại nhiều lợi ích. Thứ nhất, giao diện được chia thành các thành phần nhỏ, giúp mã nguồn dễ đọc và dễ bảo trì. Thứ hai, các thuộc tính như chủ đề, màu sắc hoặc ngữ cảnh có thể được truyền xuống cây thông qua các cơ chế sẵn có của Flutter. Thứ ba, việc xây dựng giao diện theo cấu trúc lồng nhau phù hợp với tư duy thành phần, trong đó mỗi widget đảm nhiệm một trách nhiệm tương đối rõ ràng.

Cần phân biệt widget tree với element tree và render tree. Widget tree là bản mô tả cấu hình. Element tree giữ mối liên hệ giữa widget và vị trí của nó trong giao diện, đồng thời duy trì trạng thái của các widget có trạng thái. Render tree chứa các đối tượng chịu trách nhiệm về layout và việc vẽ. Việc tách các tầng này giúp Flutter cập nhật giao diện hiệu quả mà không cần dựng lại toàn bộ mọi thứ từ đầu.

3. StatelessWidget: widget không có trạng thái nội tại

StatelessWidget phù hợp với những thành phần mà giao diện chỉ phụ thuộc vào các giá trị đầu vào và không cần thay đổi trạng thái nội tại trong suốt vòng đời của widget.

Ví dụ:

class WelcomeMessage extends StatelessWidget {
  final String name;
  const WelcomeMessage({super.key, required this.name});
  @override
  Widget build(BuildContext context) {
    return Text(
      'Chào mừng, $name!',
      style: const TextStyle(fontSize: 20),
    );
  }
}

WelcomeMessage nhận name làm dữ liệu đầu vào. Khi name không đổi, nội dung của widget cũng không cần thay đổi. Phương thức build trả về mô tả giao diện tương ứng.

Tên gọi “không có trạng thái” không có nghĩa là widget này không bao giờ xuất hiện trong một giao diện động. Một StatelessWidget vẫn có thể hiển thị dữ liệu thay đổi nếu dữ liệu đó được truyền từ widget cha hoặc đến từ một nguồn bên ngoài. Điều quan trọng là bản thân widget không sở hữu trạng thái có thể thay đổi thông qua setState.

Nên ưu tiên StatelessWidget khi:

  • Widget chỉ trình bày dữ liệu.
  • Giao diện được xác định hoàn toàn bởi các thuộc tính đầu vào.
  • Không có tương tác làm thay đổi dữ liệu nội bộ.
  • Muốn tạo một thành phần nhỏ, tái sử dụng và dễ kiểm thử.

4. StatefulWidget: widget có trạng thái thay đổi

StatefulWidget được sử dụng khi giao diện cần phản ứng với những thay đổi trong thời gian chạy, chẳng hạn như trạng thái một hộp kiểm, số lần nhấn nút, trạng thái tải dữ liệu hoặc nội dung của trường nhập liệu.

Một StatefulWidget gồm hai phần: lớp widget bất biến và lớp State chứa dữ liệu có thể thay đổi.

class Counter extends StatefulWidget {
  const Counter({super.key});
  @override
  State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
  int count = 0;
  void increment() {
    setState(() {
      count++;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text('Số lần nhấn: $count'),
        ElevatedButton(
          onPressed: increment,
          child: const Text('Tăng'),
        ),
      ],
    );
  }
}

Khi người dùng nhấn nút, phương thức increment gọi setState. Lời gọi này thông báo cho Flutter rằng trạng thái liên quan đã thay đổi và widget cần được xây dựng lại. Flutter sẽ gọi lại build để tạo mô tả giao diện mới.

Điều cần lưu ý là setState không trực tiếp vẽ lại màn hình theo nghĩa thủ công. Nó chỉ đánh dấu rằng phần trạng thái tương ứng đã thay đổi. Flutter sau đó thực hiện quá trình đánh giá và cập nhật các phần cần thiết.

5. Phương thức build và nguyên tắc xây dựng lại giao diện

Phương thức build là trung tâm của quá trình mô tả giao diện. Nó nhận BuildContext và trả về một widget.

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text('Trang chủ')),
    body: const Center(child: Text('Nội dung')),
  );
}

Một quan niệm sai phổ biến là build chỉ được gọi một lần. Trên thực tế, Flutter có thể gọi lại build khi trạng thái thay đổi, khi widget cha được xây dựng lại, khi kích thước hoặc môi trường giao diện thay đổi, hoặc khi một phụ thuộc trong BuildContext thay đổi.

Vì vậy, build nên được viết theo hướng:

  • Không chứa tác vụ tốn thời gian.
  • Không thực hiện gọi mạng hoặc ghi dữ liệu trực tiếp.
  • Không có tác dụng phụ không cần thiết.
  • Chỉ tập trung tạo ra mô tả giao diện từ dữ liệu hiện tại.

Các tác vụ bất đồng bộ thường nên được khởi tạo trong initState, trong một lớp quản lý trạng thái riêng hoặc thông qua cơ chế phù hợp khác. Nếu thực hiện gọi mạng trực tiếp trong build, ứng dụng có thể gọi lại tác vụ nhiều lần ngoài ý muốn.

6. BuildContext và vị trí của widget trong cây

BuildContext đại diện cho vị trí của một widget trong element tree. Nó cho phép widget truy cập vào các thông tin và đối tượng ở gần vị trí đó, chẳng hạn như theme, kích thước màn hình, navigator hoặc các inherited widget.

Một số cách sử dụng phổ biến:

final theme = Theme.of(context);
final screenWidth = MediaQuery.sizeOf(context).width;
Navigator.of(context).push(
  MaterialPageRoute(builder: (_) => const DetailPage()),
);

BuildContext không phải là một widget và cũng không nên được xem như một “kho dữ liệu” chung. Giá trị của nó phụ thuộc vào vị trí trong cây. Vì thế, cần cẩn thận khi dùng context trong các tác vụ bất đồng bộ. Sau khi await, widget có thể đã bị loại khỏi cây. Trong những trường hợp đó, nên kiểm tra mounted trước khi tiếp tục sử dụng context hoặc cập nhật trạng thái.

7. Hệ thống layout: constraints go down, sizes go up, parent sets position

Một trong những nguyên tắc quan trọng nhất của Flutter layout là:

     “Constraints đi xuống, kích thước đi lên, và widget cha quyết định vị trí.”

Widget cha truyền các giới hạn kích thước cho widget con. Widget con chọn một kích thước phù hợp trong giới hạn đó và trả kết quả lên. Sau đó, widget cha đặt widget con vào vị trí cụ thể.

Ví dụ, Container có thể nhận chiều rộng và chiều cao từ cha, rồi áp dụng thêm các thuộc tính như padding, margin hoặc decoration. Row và Column phân phối không gian theo trục ngang hoặc dọc. Expanded yêu cầu con chiếm phần không gian còn lại trong một Flex như Row hoặc Column.

Một lỗi thường gặp là đặt một widget có kích thước không giới hạn vào nơi yêu cầu kích thước hữu hạn, chẳng hạn đặt ListView bên trong Column mà không dùng Expanded hoặc Flexible.

Column(
  children: [
    const Text('Danh sách'),
    Expanded(
      child: ListView.builder(
        itemCount: 20,
        itemBuilder: (context, index) {
          return ListTile(title: Text('Mục $index'));
        },
      ),
    ),
  ],
)

Trong ví dụ này, Expanded cung cấp cho ListView một chiều cao hữu hạn, giúp hệ thống layout hoạt động đúng.

8. Composition: sức mạnh của việc kết hợp widget

Flutter khuyến khích composition, tức xây dựng widget lớn từ các widget nhỏ hơn, thay vì tạo ra một lớp widget khổng lồ xử lý mọi thứ.

Ví dụ, một thẻ sản phẩm có thể được tạo từ Card, Padding, Row, Image, Column, Text và Button. Mỗi phần đảm nhận một nhiệm vụ cụ thể. Khi thiết kế hợp lý, các thành phần như tiêu đề, giá tiền hoặc nút hành động có thể được tái sử dụng ở nhiều màn hình.

Composition giúp:

  • Giảm độ phức tạp của từng lớp.
  • Tăng khả năng tái sử dụng.
  • Dễ kiểm thử từng phần.
  • Làm cho cấu trúc giao diện gần với cấu trúc trực quan của sản phẩm.
  • Hạn chế việc kế thừa sâu và logic khó theo dõi.

Một widget nên được tách nhỏ khi nó có trách nhiệm riêng, được sử dụng nhiều lần hoặc khiến phương thức build trở nên quá dài và khó đọc.

9. Các nhóm widget thường gặp

Flutter cung cấp một hệ sinh thái widget phong phú. Một số nhóm quan trọng gồm:

Widget hiển thị nội dung

  • Text: hiển thị văn bản.
  • Icon: hiển thị biểu tượng.
  • Image: hiển thị hình ảnh.
  • Card: tạo bề mặt dạng thẻ.

Widget bố cục

  • Row: sắp xếp con theo chiều ngang.
  • Column: sắp xếp con theo chiều dọc.
  • Stack: xếp chồng các widget.
  • Wrap: tự động xuống dòng khi không đủ không gian.
  • Container: hỗ trợ kích thước, padding, margin và trang trí.
  • Padding, Align, Center, SizedBox: điều chỉnh khoảng cách, vị trí và kích thước.

Widget tương tác

  • ElevatedButton và TextButton: nút bấm.
  • GestureDetector: phát hiện cử chỉ.
  • InkWell: tương tác có hiệu ứng Material.
  • TextField: nhập dữ liệu.
  • Checkbox, Switch, Radio: điều khiển lựa chọn.

Widget điều hướng và khung ứng dụng

  • MaterialApp: khung ứng dụng theo Material Design.
  • CupertinoApp: khung ứng dụng theo phong cách iOS.
  • Scaffold: cung cấp cấu trúc cơ bản của một màn hình.
  • Navigator: quản lý ngăn xếp các màn hình.

Việc chọn widget phù hợp không chỉ ảnh hưởng đến giao diện mà còn ảnh hưởng đến khả năng truy cập, hiệu năng và tính nhất quán của ứng dụng.

10. Quản lý trạng thái và ranh giới trách nhiệm

Trạng thái có thể được chia thành nhiều loại. Ephemeral state là trạng thái cục bộ, tồn tại trong phạm vi một widget hoặc một màn hình, chẳng hạn việc một nút đang được chọn. App state là trạng thái dùng chung ở nhiều nơi, chẳng hạn thông tin người dùng đăng nhập, giỏ hàng hoặc cài đặt ứng dụng.

Với trạng thái cục bộ đơn giản, setState thường là lựa chọn phù hợp. Tuy nhiên, khi ứng dụng phát triển, việc truyền dữ liệu qua nhiều tầng widget có thể trở nên khó quản lý. Khi đó, có thể cân nhắc các giải pháp như InheritedWidget, ChangeNotifier, Provider, Riverpod, Bloc hoặc các kiến trúc quản lý trạng thái khác.

Điểm quan trọng không phải là sử dụng thư viện nào, mà là xác định rõ:

  • Trạng thái thuộc về widget nào?
  • Widget nào có quyền thay đổi trạng thái?
  • Widget nào chỉ đọc trạng thái?
  • Dữ liệu cần tồn tại trong bao lâu?
  • Có cần chia sẻ dữ liệu giữa nhiều màn hình hay không?

Một thiết kế tốt đưa trạng thái đến nơi gần nhất có thể, nhưng đủ cao để mọi thành phần cần dùng đều truy cập được.

11. Key và nhận diện widget

Flutter thường nhận diện widget dựa trên vị trí trong cây và kiểu widget. Khi danh sách thay đổi hoặc các widget cùng loại được sắp xếp lại, Flutter có thể cần thêm thông tin để biết phần tử nào tương ứng với trạng thái nào. Đây là vai trò của Key.

Ví dụ:

ListView(
  children: const [
    UserTile(key: ValueKey('user-1'), name: 'An'),
    UserTile(key: ValueKey('user-2'), name: 'Bình'),
  ],
)

Các loại key thường gặp gồm ValueKey, ObjectKey, UniqueKey và GlobalKey. Trong đa số trường hợp với danh sách, ValueKey là lựa chọn rõ ràng và an toàn khi mỗi phần tử có một định danh ổn định.

GlobalKey có khả năng mạnh hơn, nhưng không nên lạm dụng vì có thể làm tăng độ phức tạp và chi phí quản lý cây widget. Key nên được dùng khi cần bảo toàn trạng thái, nhận diện phần tử khi thứ tự thay đổi hoặc truy cập một số thông tin đặc biệt của widget.

12. Vòng đời của StatefulWidget

Một StatefulWidget có vòng đời thường được mô tả qua các phương thức sau:

  • createState: tạo đối tượng trạng thái.
  • initState: khởi tạo dữ liệu và đăng ký tài nguyên; được gọi một lần.
  • didChangeDependencies: được gọi khi các phụ thuộc inherited thay đổi.
  • build: xây dựng giao diện.
  • didUpdateWidget: được gọi khi widget cấu hình thay đổi nhưng state vẫn được giữ lại.
  • deactivate: state tạm thời bị loại khỏi cây.
  • dispose: giải phóng tài nguyên khi state không còn được sử dụng.

Ví dụ, nếu một widget sử dụng TextEditingController, AnimationController hoặc StreamSubscription, các tài nguyên đó cần được giải phóng trong dispose.

class SearchBox extends StatefulWidget {
  const SearchBox({super.key});
  @override
  State<SearchBox> createState() => _SearchBoxState();
}
class _SearchBoxState extends State<SearchBox> {
  final controller = TextEditingController();
  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return TextField(controller: controller);
  }
}

Quản lý vòng đời đúng giúp tránh rò rỉ bộ nhớ, lỗi cập nhật state sau khi widget đã bị hủy và các hành vi không ổn định.

13. Hiệu năng và khả năng bảo trì

Flutter thường xử lý việc xây dựng lại widget khá hiệu quả, nhưng điều đó không có nghĩa là mọi mã nguồn đều tự động tối ưu. Một số nguyên tắc quan trọng gồm:

  • Dùng const khi có thể: Widget hằng giúp giảm việc tạo đối tượng lặp lại và thể hiện rõ rằng cấu hình không thay đổi.
  • Tách widget lớn thành các widget nhỏ: Điều này giúp giới hạn phạm vi rebuild và làm mã nguồn dễ hiểu hơn.
  • Dùng builder cho danh sách dài: ListView.builder.builder tạo phần tử theo nhu cầu thay vì tạo toàn bộ danh sách ngay lập tức.
  • Tránh tác vụ nặng trong build: Tính toán phức tạp, xử lý dữ liệu lớn hoặc gọi mạng nên được đưa ra ngoài.
  • Chọn đúng widget layout: Lồng quá nhiều Row, Column hoặc IntrinsicHeight không cần thiết có thể làm layout tốn chi phí hơn.
  • Theo dõi bằng DevTools: Flutter DevTools hỗ trợ kiểm tra widget tree, layout, rebuild và hiệu năng khung hình.

Hiệu năng tốt không chỉ đến từ những tối ưu nhỏ, mà còn từ việc tổ chức trạng thái và ranh giới widget một cách hợp lý.

14. Khả năng truy cập và trải nghiệm người dùng

Widget fundamentals cũng bao gồm việc tạo giao diện có thể sử dụng bởi nhiều nhóm người dùng. Nên cung cấp nhãn rõ ràng cho các nút biểu tượng, bảo đảm độ tương phản phù hợp, hỗ trợ kích thước chữ lớn hơn và tránh phụ thuộc hoàn toàn vào màu sắc để truyền đạt ý nghĩa.

Flutter cung cấp Semantics và nhiều thuộc tính hỗ trợ khả năng truy cập. Chẳng hạn, một biểu tượng chỉ có ý nghĩa trực quan có thể cần được gắn nhãn để công cụ đọc màn hình hiểu đúng chức năng của nó.

Ngoài ra, layout cần thích ứng với nhiều kích thước màn hình. MediaQuery, LayoutBuilder, Flexible, Expanded và Wrap là những công cụ thường được dùng để xây dựng giao diện phản hồi tốt trên điện thoại, máy tính bảng và màn hình lớn.

15. Những sai lầm phổ biến của người mới học

Một số lỗi thường gặp gồm:

  • Cho rằng mọi thay đổi giao diện đều cần StatefulWidget.
  • Đặt quá nhiều logic nghiệp vụ trong build.
  • Gọi API trực tiếp trong build.
  • Không dùng Expanded khi đặt danh sách cuộn trong Column.
  • Lạm dụng GlobalKey.
  • Không giải phóng controller, animation hoặc subscription.
  • Truyền dữ liệu qua quá nhiều tầng mà không xem xét lại thiết kế trạng thái.
  • Tạo một widget duy nhất quá lớn, khó kiểm thử và khó tái sử dụng.
  • Bỏ qua const, khiến mã nguồn kém rõ ràng và có thêm các lần tạo đối tượng không cần thiết.

Hiểu nguyên nhân của những lỗi này quan trọng hơn việc chỉ ghi nhớ cách sửa. Hầu hết chúng đều xuất phát từ việc chưa nắm rõ mối quan hệ giữa widget, state, element và layout constraints.

Kết luận

Flutter Widget Fundamentals là nền tảng để hiểu toàn bộ cách Flutter xây dựng giao diện. Widget là những mô tả bất biến, được tổ chức thành cây và được Flutter sử dụng để tạo ra giao diện tương tác. StatelessWidget phù hợp với các thành phần phụ thuộc vào dữ liệu đầu vào, trong khi StatefulWidget quản lý những trạng thái thay đổi theo thời gian. Phương thức build, BuildContext, hệ thống constraints, composition, key và vòng đời state là những khái niệm liên kết chặt chẽ với nhau.

Khi nắm vững các nguyên tắc này, lập trình viên không chỉ có thể viết được giao diện Flutter hoạt động đúng mà còn tạo ra mã nguồn dễ mở rộng, dễ kiểm thử và có hiệu năng tốt. Tư duy quan trọng nhất là xem giao diện như một hàm của trạng thái hiện tại: khi trạng thái thay đổi, Flutter được cung cấp một mô tả mới và sẽ cập nhật phần giao diện cần thiết. Từ nền tảng đó, người học có thể tiếp tục khám phá quản lý trạng thái nâng cao, animation, navigation, accessibility, testing và kiến trúc ứng dụng quy mô lớn.

Bài viết khác

Isolate và Concurrency

     Trong quá trình phát triển phần mềm hiện đại, ứng dụng không chỉ cần cho ra kết quả đúng mà còn phải phản hồi nhanh, sử dụng tài nguyên hiệu quả và có khả năng xử lý nhiều công việc cùng lúc. Khi người dùng mở một ứng dụng di động, họ kỳ […]

Flutter Layout & Responsive UI

Flutter Layout & Responsive UI 1. Flutter Layout Layout trong Flutter là quá trình xác định kích thước và vị trí của các Widget trên màn hình. Flutter sử dụng hệ thống Widget để xây dựng giao diện. Các Widget Layout quyết định: Widget nằm ở đâu. Widget có kích thước bao nhiêu. Các Widget […]

StatelessWidget vs StatefulWidget

StatelessWidget vs StatefulWidget 1. Khái niệm Trong Flutter, StatelessWidget và StatefulWidget là hai loại Widget cơ bản dùng để xây dựng giao diện. Điểm khác biệt quan trọng nhất nằm ở State (trạng thái). StatelessWidget: Widget không có State nội bộ có thể thay đổi. StatefulWidget: Widget có State nội bộ có thể thay đổi trong quá […]

Flutter Widget Fundamentals

Flutter Widget Fundamentals 1. Widget Trong Flutter, Widget là thành phần cơ bản dùng để xây dựng giao diện người dùng (UI). Có thể hiểu đơn giản: Mọi thành phần xuất hiện trên giao diện Flutter đều được xây dựng từ Widget. Ví dụ: Text → hiển thị văn bản. Image → hiển thị hình […]

Isolate & Concurrency

Isolate & Concurrency Isolate và Concurrency (đồng thời) là cơ chế trong Dart dùng để thực hiện nhiều công việc mà không làm một công việc phải chờ hoàn thành hoàn toàn mới có thể xử lý công việc khác. Lý thuyết 1. Concurrency Concurrency (xử lý đồng thời) là khả năng chương trình quản […]

Null Safety trong Dart

Null Safety là cơ chế phân biệt rõ ràng giữa kiểu dữ liệu có thể chứa null (Nullable) và kiểu dữ liệu không thể chứa null (Non-nullable) ngay ở mức ngôn ngữ lập trình, giúp trình biên dịch phát hiện và bắt lỗi liên quan đến null ngay trong quá trình viết mã thay vì […]

Leave a Reply

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