Hướng Nghiệp Dữ Liệu

Scaffold, AppBar, Drawer là gì? Cách xây dựng layout chuẩn Material

Được viết bởi Admin|0 lượt xem
Scaffold, AppBar, Drawer là gì? Cách xây dựng layout chuẩn Material

Tóm tắt nhanh

Tìm hiểu về Scaffold, AppBar và Drawer trong Flutter - các widget cơ bản để xây dựng layout Material Design chuẩn.

Mục lục bài viết (48)

Scaffold, AppBar và Drawer là các widget cơ bản để xây dựng layout Material Design trong Flutter. Bài viết này sẽ hướng dẫn bạn sử dụng chúng.

Scaffold là gì?

Scaffold là widget cung cấp cấu trúc cơ bản cho Material Design app.

Cấu trúc:

[object Object]

Ví dụ cơ bản:

[object Object]

AppBar

1. AppBar cơ bản

[object Object]

2. AppBar với actions

[object Object]

3. AppBar với leading

[object Object]

4. AppBar với flexibleSpace

[object Object]

Drawer

1. Drawer cơ bản

[object Object]

2. Drawer với User Account

[object Object]

Layout hoàn chỉnh

1. Standard Layout

[object Object]

Best Practices

1. Consistent AppBar

[object Object]

2. Drawer Navigation

[object Object]

Scaffold thực chất quản lý những vùng nào?

Nhiều người dùng Scaffold như một khung trang trí, nhưng thực tế nó chịu trách nhiệm khá nhiều việc khó thấy:

  • Đặt vị trí cho appBar, drawer, endDrawer, bottomNavigationBar, floatingActionButton và body, đồng thời tính chiều cao thật của body sau khi trừ các phần đó.
  • Cung cấp ScaffoldMessenger để hiện SnackBar và MaterialBanner đúng ngữ cảnh màn hình.
  • Xử lý vùng an toàn (tai thỏ, thanh cử chỉ) và điều chỉnh khi bàn phím mở, thông qua resizeToAvoidBottomInset.
  • Mở và đóng drawer, xử lý cử chỉ vuốt từ cạnh màn hình.

Vì những việc này thuộc về Scaffold, quy tắc thực dụng là: mỗi màn hình có đúng một Scaffold, và chỉ lồng Scaffold khi bạn thật sự muốn một khung riêng — ví dụ tab con có thanh công cụ riêng bên dưới thanh công cụ của màn hình cha.

AppBar: những tham số ít người dùng nhưng rất hữu ích

Phần lớn ví dụ chỉ dùng title và actions. Nhưng khi làm sản phẩm thật, bốn tham số sau dùng thường xuyên hơn cả:

[object Object]

Ba điểm đáng chú ý:

  • Luôn đặt tooltip cho IconButton chỉ có icon. Ngoài chuyện người dùng mơ hồ về chức năng, đây còn là yêu cầu về khả năng truy cập.
  • centerTitle khác nhau mặc định giữa Android và iOS. Nếu thiết kế đã xác định, hãy đặt giá trị rõ ràng thay vì để mặc định theo nền tảng.
  • bottom nhận một widget tuỳ ý, không bắt buộc là TabBar. Đây là chỗ để gắn một thanh tìm kiếm hoặc SegmentedButton. Lưu ý tham số này làm tăng chiều cao AppBar, nên preferredSize cũng thay đổi theo.

Nếu bạn cần gắn AppBar vào một danh sách cuộn được để nó trượt theo nội dung, dùng SliverAppBar trong CustomScrollView thay vì AppBar đặt trong Scaffold.

Drawer: ba lỗi chỉ xuất hiện khi chạy thật

Đóng drawer trước khi điều hướng. Nếu không, drawer vẫn đang mở trên màn hình mới. Cách viết đúng như ví dụ best practice ở trên: Navigator.pop(context) trước, rồi mới điều hướng. Với go_router, dùng context.pop() rồi context.go(...) — xem thêm bài so sánh Navigation 1.0 và 2.0.

Dùng sai context để mở drawer. Bên trong body, Scaffold.of(context) sẽ tìm Scaffold gần nhất phía trên widget hiện tại, nhưng context của chính Scaffold không nằm phía trên nó. Cách chắc chắn: dùng Builder để lấy context con, hoặc dùng GlobalKey<ScaffoldState>.

[object Object]

Quên endDrawer cho thao tác phụ. Scaffold có sẵn endDrawer mở từ cạnh phải — phù hợp cho bộ lọc, thông tin tài khoản, hoặc giỏ hàng, mà không phải cài thêm thư viện.

Thanh điều hướng dưới: giữ trạng thái từng tab

Vấn đề kinh điển: bấm sang tab khác rồi quay lại, danh sách bị mất vị trí cuộn và tải lại từ đầu. Nguyên nhân là widget của tab cũ bị xoá khỏi cây.

[object Object]

IndexedStack giữ cả ba tab trong cây nên trạng thái và vị trí cuộn được bảo toàn. Đổi lại, cả ba tab đều được dựng ngay lần đầu. Nếu tab nào quá nặng, hãy khởi tạo dữ liệu muộn (lazy) thay vì đổi sang xoá widget.

Một lưu ý về phiên bản Material: BottomNavigationBar thuộc Material 2, còn NavigationBar là widget của Material 3 với cách viết destinations như trên. Chọn theo thiết kế đang dùng và giữ nhất quán trong toàn app.

Bố cục thích ứng: điện thoại dùng Drawer, tablet dùng thanh bên

Một drawer mở ra che mất nửa màn hình là trải nghiệm tốt trên điện thoại, nhưng lại lãng phí trên tablet và web. Cách làm đúng là chọn cấu trúc theo chiều rộng màn hình.

[object Object]

Điểm cần chú ý: MediaQuery.sizeOf(context) chỉ đăng ký lắng nghe phần kích thước, nên ít gây rebuild hơn MediaQuery.of(context). Chi tiết về bố cục thích ứng ở bài LayoutBuilder và MediaQuery và bài responsive design trong Flutter.

Ba lỗi hay gặp khác

  1. Thanh công cụ bị che mất khi bàn phím mở. Nguyên nhân thường là body dùng Column không cuộn được. Hãy để nội dung nằm trong widget cuộn (ListView, SingleChildScrollView) và để resizeToAvoidBottomInset xử lý phần còn lại.
  2. Nội dung chạy dưới thanh trạng thái. Xảy ra khi dùng nền toàn màn hình mà không có SafeArea. Chỉ bọc SafeArea ở nơi thật sự cần, vì bọc toàn bộ sẽ tạo khoảng trống không mong muốn ở các vùng đã có AppBar.
  3. Đặt SnackBar sai chỗ. ScaffoldMessenger.of(context).showSnackBar cần một ScaffoldMessenger phía trên; trong luồng có nhiều Scaffold lồng nhau, thông báo có thể hiện ở màn hình khác với nơi bạn muốn.

Câu hỏi thường gặp

Có thể dùng nhiều Scaffold trong cùng một màn hình không?

Có, khi bạn muốn một vùng có khung riêng — ví dụ tab con có thanh công cụ riêng. Nhưng nên hạn chế, vì mỗi Scaffold thêm một tầng xử lý vùng an toàn và có thể làm SnackBar hiện không đúng vị trí mong đợi.

Drawer mở bằng cử chỉ vuốt ở cạnh màn hình có tắt được không?

Được, bằng drawerEnableOpenDragGesture: false trên Scaffold. Cần tắt khi màn hình đó có thành phần kéo ngang khác, chẳng hạn biểu đồ hoặc băng chuyền ảnh, vì hai cử chỉ sẽ tranh nhau.

Nên đặt AppBar ở đâu trong dự án có nhiều màn hình giống nhau?

Tách thành một widget dùng chung, nhận title và actions qua tham số. Nhờ vậy khi đổi thiết kế thanh tiêu đề, bạn sửa một chỗ thay vì hàng chục file — và đây cũng là câu hỏi về tổ chức code thường gặp khi phỏng vấn, xem bộ câu hỏi phỏng vấn Flutter.

AppBar trong suốt trên ảnh nền thì làm thế nào?

Đặt backgroundColor: Colors.transparent, elevation: 0, và bọc nội dung trong Stack hoặc dùng flexibleSpace chứa ảnh. Cần kiểm tra độ tương phản của chữ để bảo đảm vẫn đọc được trên ảnh sáng.

ScaffoldMessenger: hiện thông báo đúng chỗ, đúng lúc

SnackBar là thành phần bị dùng sai nhiều nhất trong nhóm widget này, và cái sai thường không lộ ra ngay mà chỉ xuất hiện ở một vài luồng.

Vấn đề 1 — thông báo hiện trên màn hình khác. ScaffoldMessenger.of(context) tìm ScaffoldMessenger gần nhất phía trên. Trong ứng dụng có nhiều Scaffold lồng nhau, thông báo có thể hiện ở màn hình mà người dùng đã rời đi. Cách xử lý: lấy ScaffoldMessenger trước khi await, rồi dùng lại biến đó sau.

[object Object]

Vấn đề 2 — thông báo xếp hàng dài. Mặc định, các SnackBar được đưa vào hàng đợi và hiện lần lượt. Người dùng bấm nhanh nhiều lần sẽ phải chờ xem hết các thông báo cũ. Cách sửa: gọi hideCurrentSnackBar() trước khi hiện thông báo mới, hoặc removeCurrentSnackBar() nếu muốn bỏ ngay lập tức.

[object Object]

Vấn đề 3 — thông báo quan trọng bị bỏ qua. Với cảnh báo cần người dùng chú ý (mất kết nối, tài khoản bị khoá), SnackBar ở dưới cùng dễ bị bỏ qua. Trường hợp này nên dùng MaterialBanner, hiển thị ở trên và tồn tại cho tới khi người dùng xử lý:

[object Object]

Quy tắc chọn nhanh: SnackBar cho phản hồi tạm thời của một hành động, MaterialBanner cho trạng thái kéo dài cần người dùng biết, AlertDialog cho quyết định bắt buộc phải trả lời.

Drawer với ListView vẫn dùng tốt, nhưng Material 3 có NavigationDrawer gọn hơn và tự xử lý trạng thái mục đang chọn.

[object Object]

Ba điểm đáng lưu ý khi làm drawer nhiều mục:

  • Luôn Navigator.pop(context) trước khi điều hướng. Nếu quên, drawer sẽ vẫn mở trên màn hình mới.
  • Chia nhóm có tiêu đề khi drawer quá khoảng 7 mục. Danh sách phẳng dài rất khó quét mắt.
  • Chỉ dùng drawer cho điều hướng cấp cao nhất. Nếu mỗi màn hình lại có mục riêng trong drawer, người dùng sẽ mất phương hướng về vị trí hiện tại của mình.

AppBar với ô tìm kiếm

Cách làm nhanh nhất là mở rộng actions bằng một nút mở trang tìm kiếm. Cách hiện đại hơn là dùng SearchAnchor, cho phép mở lớp phủ tìm kiếm với gợi ý ngay trên thanh hiện tại:

[object Object]

Hai điều cần chú ý: suggestionsBuilder được gọi mỗi khi nội dung thay đổi, nên việc gọi API cần chống dội ở tầng service chứ không chỉ ở giao diện; và kết quả trả về muộn có thể ghi đè kết quả mới hơn nếu bạn không kiểm tra từ khoá hiện tại.

Nếu chỉ cần tìm kiếm trong dữ liệu đã có, hãy lọc ngay trong bộ nhớ và không cần gọi mạng.

SliverAppBar nâng cao: pinned, floating, snap và thanh tab

Khi cần thanh tiêu đề thu gọn theo nội dung, SliverAppBar là công cụ phù hợp, và ba cờ của nó thay đổi hành vi khá nhiều:

[object Object]
  • pinned giữ thanh tiêu đề luôn hiển thị, chỉ thu nhỏ chiều cao.
  • floating cho thanh tiêu đề ẩn khi cuộn xuống và hiện lại khi cuộn lên.
  • snap bắt buộc đi cùng floating, giúp thanh tiêu đề hiện ra hoàn toàn hoặc không hiện gì, tránh trạng thái nửa vời khi người dùng dừng tay giữa chừng.

Nếu muốn ảnh bìa co giãn khi kéo quá đầu danh sách (hiệu ứng thường thấy ở trang chi tiết sản phẩm), thêm stretch: true và stretchModes.

TabBar và TabBarView: giữ trạng thái từng tab

Mặc định TabBarView xoá widget của tab khi chuyển đi. Với tab có danh sách, điều đó đồng nghĩa mất vị trí cuộn và tải lại dữ liệu — trải nghiệm rất tệ. Hai cách xử lý:

[object Object]

Cách 2 là chuyển hẳn sang StatefulShellRoute của go_router nếu ứng dụng đã dùng router khai báo — mỗi nhánh giữ ngăn xếp và trạng thái riêng, như đã trình bày ở bài so sánh Navigation 1.0 và 2.0.

Một chi tiết nhỏ khi dùng DefaultTabController: nó phải nằm phía trên cả AppBar và TabBarView trong cây widget. Nếu đặt sai vị trí, bạn gặp lỗi không tìm thấy TabController.

Vùng an toàn, bàn phím và edge-to-edge

Trên các thiết bị đời mới, giao diện tràn ra sát mép màn hình — vừa đẹp vừa dễ sai. Ba khái niệm cần phân biệt:

  • MediaQuery.padding — vùng bị che bởi tai thỏ, thanh trạng thái, thanh cử chỉ.
  • MediaQuery.viewInsets — vùng bị che bởi bàn phím.
  • MediaQuery.viewPadding — tương tự padding nhưng không bị ảnh hưởng bởi bàn phím.

Trong phần lớn trường hợp, chỉ cần SafeArea ở những chỗ nội dung sát mép. Nhưng khi bạn muốn ảnh nền tràn ra toàn màn hình mà nội dung vẫn an toàn, hãy đặt SafeArea bên trong và cho ảnh nằm ngoài:

[object Object]

Với bàn phím, thứ tự xử lý đúng là: để Scaffold tự điều chỉnh (resizeToAvoidBottomInset mặc định đã bật), cho nội dung nằm trong widget cuộn được, và dùng bottomNavigationBar hoặc bottomSheet cho thanh hành động nếu muốn nó nổi trên bàn phím. Nếu bạn tự tính toán bằng viewInsets.bottom, gần như chắc chắn sẽ gặp trường hợp lệch ở một số thiết bị.

Theming: đổi giao diện AppBar theo theme

Đừng hard-code màu trong AppBar. Lấy từ theme để chế độ sáng/tối hoạt động đúng:

[object Object]

scrolledUnderElevation là tham số dành riêng cho hành vi khi nội dung cuộn lên dưới thanh tiêu đề: Flutter tự thêm bóng nhẹ để phân tách hai lớp. Đây là cách làm đúng thay vì tự bật/tắt bóng theo sự kiện cuộn.

Bố cục nhiều cột trên tablet và web

Ở màn hình rộng, AppBar và Drawer là chưa đủ để tận dụng không gian. Cấu trúc thường dùng:

[object Object]

Nguyên tắc: thêm cột, không kéo giãn. Nếu chỉ phóng to giao diện điện thoại trên tablet, mỗi dòng chữ sẽ dài tới mức khó đọc. Thay vào đó hãy dùng không gian thừa cho một cột nội dung phụ — thông tin khoá học liên quan, mục lục, hoặc danh sách bài cùng chuyên mục.

Test mở drawer và hiện thông báo

Hai hành vi này nên có test vì rất dễ hỏng khi refactor:

[object Object]

Lưu ý kỹ thuật: SnackBar xuất hiện sau một khoảng ngắn, nên cần await tester.pump() rồi mới khẳng định — dùng pumpAndSettle() ngay có thể không ổn định vì SnackBar có thời lượng tự đóng.

Câu hỏi thường gặp (tiếp)

Vì sao Scaffold.of(context) báo lỗi?

Vì context bạn dùng là của chính widget Scaffold hoặc của widget cha nó, không phải của widget con bên trong body. Scaffold.of chỉ tìm được Scaffold phía trên context. Hãy dùng Builder, GlobalKey<ScaffoldState>, hoặc gọi từ một widget con thực sự.

BottomNavigationBar và NavigationBar khác nhau ở đâu?

NavigationBar là widget Material 3, dùng destinations với NavigationDestination, hỗ trợ chỉ báo dạng viên thuốc và tự xử lý nhãn. BottomNavigationBar thuộc Material 2. Không nên trộn cả hai trong cùng ứng dụng vì trông sẽ lệch nhau về bo góc, độ cao và cách hiển thị nhãn.

AppBar bị che khi bàn phím mở thì sửa thế nào?

Kiểm tra hai thứ: nội dung body có nằm trong widget cuộn được không, và resizeToAvoidBottomInset có bị đặt false không. Nếu đã đúng mà vẫn lỗi, hãy kiểm tra widget cha của Scaffold — nếu Scaffold nằm trong một widget không co giãn, phần điều chỉnh của nó sẽ không có tác dụng.

Nên dùng AppBar hay tự dựng thanh tiêu đề?

Dùng AppBar trừ khi thiết kế yêu cầu khác biệt lớn. AppBar đã xử lý vùng an toàn, đổ bóng khi cuộn, hành vi nút Back, và tính tương thích với theme. Tự dựng gần như luôn dẫn tới một danh sách lỗi nhỏ phải sửa dần.

Làm sao để drawer luôn hiện đúng mục đang xem?

Truyền chỉ số mục hiện tại vào drawer và đánh dấu mục đó. Với NavigationDrawer, đặt selectedIndex. Với drawer tự dựng bằng ListTile, truyền selected: true và để Material tô nền khác cho mục đó. Quan trọng hơn cả phần hiển thị: mỗi màn hình nên có một định danh rõ ràng (ví dụ tên tuyến đường) để biết mục nào đang chọn, thay vì tự đếm chỉ số trong từng màn hình — cách đó rất dễ lệch khi thêm hoặc bỏ mục.

AppBar có hỗ trợ tiêu đề dài hai dòng không?

Có, nhưng cần cân nhắc. AppBar có title là một widget nên bạn có thể đặt Column với hai dòng. Tuy nhiên chiều cao mặc định của thanh tiêu đề chỉ đủ cho một dòng, nên tiêu đề dài sẽ bị cắt. Hai lựa chọn thực tế: tăng toolbarHeight và chấp nhận thanh tiêu đề cao hơn, hoặc để tiêu đề ngắn và đặt tên đầy đủ trong nội dung bên dưới. Cách thứ hai thường cho giao diện gọn hơn, nhất là trên màn hình nhỏ.

Có nên ẩn thanh điều hướng dưới khi bàn phím mở?

Thường là không, vì việc ẩn đi làm giao diện nhảy và người dùng mất phương hướng. Nhưng nếu màn hình đang nhập liệu toàn màn hình, việc tạm ẩn giúp có thêm không gian cho vùng nội dung. Nếu chọn ẩn, hãy ẩn bằng hiệu ứng ngắn thay vì biến mất tức thì, và chắc chắn rằng người dùng vẫn có cách thoát khỏi màn hình đó.

Vì sao drawer không mở được khi vuốt từ cạnh màn hình?

Kiểm tra ba thứ: drawer có được truyền cho Scaffold không, cử chỉ mở drawer có bị tắt bằng drawerEnableOpenDragGesture không, và vùng cạnh màn hình đó có đang bị một widget cuộn ngang chiếm cử chỉ không. Trường hợp thứ ba là nguyên nhân phổ biến nhất trên các màn hình có băng chuyền ảnh hoặc biểu đồ.

Kết luận

Scaffold, AppBar và Drawer:

  • ✅ Scaffold: Cấu trúc cơ bản
  • ✅ AppBar: Top navigation bar
  • ✅ Drawer: Side navigation menu
  • ✅ Material Design: Chuẩn Material Design

Tóm tắt:

  1. Scaffold: Cấu trúc layout chính
  2. AppBar: Top bar với title và actions
  3. Drawer: Side menu navigation
  4. Best Practices: Consistent và user-friendly

Sử dụng Scaffold, AppBar và Drawer sẽ tạo layout Material Design chuẩn! 🚀


Học Flutter bài bản theo lộ trình

Nếu bạn muốn học có người sửa bài thay vì tự ghép tài liệu, Flutter Hướng Nghiệp đào tạo theo 3 cấp:

Lớp online trực tiếp qua Zoom/Google Meet, có bản ghi lại — không bắt buộc đến cơ sở.
Xem lịch khai giảng hoặc so sánh 5 cách học lập trình mobile nếu còn phân vân.

Đọc thêm: Flutter: Dự án xây dựng UI cho ứng dụng