Flutter Offstage: Giữ Trạng Thái, Ẩn View, Chớp Nhoáng!
Flutter

Flutter Offstage: Giữ Trạng Thái, Ẩn View, Chớp Nhoáng!

Author

Admin System

@root

Ngày xuất bản

20 Mar, 2026

Lượt xem

1 Lượt

"OffstageState"

Chào các bạn Gen Z mê code, anh Creyt đây! Hôm nay chúng ta sẽ cùng nhau "bóc phốt" một khái niệm mà nghe tên thì có vẻ "lén lút" nhưng lại cực kỳ quyền năng trong Flutter: OffstageState. Hay nói đúng hơn, là tác dụng của widget Offstage lên trạng thái của widget con.

1. OffstageState là gì mà "lén lút" dữ vậy?

Các em cứ hình dung thế này, trong vũ trụ Flutter của chúng ta, mỗi widget là một "diễn viên" trên sân khấu ứng dụng. Bình thường, khi một diễn viên không cần xuất hiện, chúng ta hay "đuổi" họ vào cánh gà (tức là xóa khỏi cây widget, chẳng hạn dùng if hoặc Visibility với maintainState: false). Khi cần lại, họ phải "trang điểm, thay đồ" lại từ đầu, khá tốn công sức và thời gian.

Offstage widget thì khác! Nó giống như một tấm màn nhung huyền bí. Khi em đặt một widget con vào trong Offstage và set thuộc tính offstage: true, thì cái widget con đó vẫn y nguyên ở trên sân khấu, vẫn giữ nguyên "trạng thái" của nó (OffstageState), nhưng bị tấm màn nhung che khuất hoàn toàn. Nó không chiếm không gian, không nhận sự kiện chạm, và không được vẽ ra màn hình. Nhưng nó vẫn "sống", vẫn "thở", vẫn "nhớ" tất cả những gì nó đang có.

Nói cách khác, Offstage giúp ta giấu đi một widget mà không cần hủy bỏ nó. Trạng thái nội tại của nó (ví dụ: giá trị của một TextField, trạng thái của một nút bấm, dữ liệu của một StreamBuilder) vẫn được bảo toàn. Cứ như một idol K-Pop đang đứng sau cánh gà, sẵn sàng bước ra trình diễn ngay lập tức, không cần phải chuẩn bị lại từ đầu vậy.

2. Code Ví Dụ: "Idol" ẩn mình và khoe dáng

Để các em dễ hình dung, chúng ta sẽ làm một ví dụ đơn giản với một CounterWidget có nút tăng giảm. Chúng ta sẽ dùng Offstage để ẩn/hiện nó và xem trạng thái của nó có được giữ nguyên không nhé.

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Offstage Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const OffstageDemoScreen(),
    );
  }
}

class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  void _decrementCounter() {
    setState(() {
      _counter--;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('CounterWidget rebuilt. Current counter: $_counter'); // Để ý log này
    return Card(
      margin: const EdgeInsets.all(16.0),
      elevation: 4,
      child: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            const Text(
              'Giá trị Counter:',
              style: TextStyle(fontSize: 18),
            ),
            Text(
              '$_counter',
              style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _decrementCounter,
                  child: const Icon(Icons.remove),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _incrementCounter,
                  child: const Icon(Icons.add),
                ),
              ],
            ),
            const SizedBox(height: 10),
            const Text(
              '(Xem log để thấy khi nào widget được rebuild)',
              style: TextStyle(fontSize: 12, fontStyle: FontStyle.italic),
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }
}

class OffstageDemoScreen extends StatefulWidget {
  const OffstageDemoScreen({super.key});

  @override
  State<OffstageDemoScreen> createState() => _OffstageDemoScreenState();
}

class _OffstageDemoScreenState extends State<OffstageDemoScreen> {
  bool _isOffstage = true; // Ban đầu ẩn CounterWidget

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Offstage Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: () {
                setState(() {
                  _isOffstage = !_isOffstage;
                });
              },
              child: Text(_isOffstage ? 'Hiện Counter' : 'Ẩn Counter'),
            ),
            const SizedBox(height: 30),
            // Đây là nơi Offstage phát huy tác dụng
            Offstage(
              offstage: _isOffstage,
              child: const CounterWidget(),
            ),
            const SizedBox(height: 30),
            const Text(
              'Widget bên dưới (luôn hiện)',
              style: TextStyle(fontSize: 16),
            ),
            // Widget này để chứng minh Offstage không ảnh hưởng layout của các widget khác
            Container(
              width: 100,
              height: 100,
              color: Colors.green,
              child: const Center(
                child: Text('Luôn Hiện', style: TextStyle(color: Colors.white)),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Thử nghiệm:

  1. Chạy ứng dụng. Ban đầu, CounterWidget bị ẩn (_isOffstagetrue).
  2. Nhấn nút "Hiện Counter". CounterWidget sẽ xuất hiện với giá trị 0.
  3. Tăng giảm counter vài lần (ví dụ lên 5).
  4. Nhấn nút "Ẩn Counter". CounterWidget biến mất. Quan sát log console: không có dòng CounterWidget rebuilt nào xuất hiện khi ẩn/hiện!
  5. Nhấn nút "Hiện Counter" lần nữa. CounterWidget xuất hiện trở lại với giá trị 5! Điều này chứng tỏ trạng thái của CounterWidget đã được giữ nguyên, không bị khởi tạo lại.
Illustration

3. Mẹo (Best Practices) của Creyt để dùng "sân khấu ẩn" hiệu quả

  • Dùng khi cần giữ trạng thái: Đây là lý do chính để dùng Offstage. Nếu em có một widget phức tạp, mất công khởi tạo, và em muốn ẩn/hiện nó mà không mất đi dữ liệu hay trạng thái hiện tại của nó, thì Offstage là lựa chọn số 1.
  • Tối ưu tốc độ chuyển đổi: Việc ẩn/hiện bằng Offstage là cực nhanh vì widget không bị xóa và tạo lại. Nó chỉ đơn giản là ngừng vẽ.
  • Cẩn trọng với hiệu năng: Mặc dù Offstage không vẽ widget con, nhưng nó vẫn giữ widget con trong cây widget (element tree và render tree). Điều này có nghĩa là nếu widget con của em cực kỳ nặng về mặt bộ nhớ hoặc có các luồng dữ liệu (stream, timer) chạy ngầm, thì việc dùng Offstage có thể không giúp tiết kiệm tài nguyên mà chỉ giấu đi thôi. Hãy cân nhắc.
  • Kết hợp với Visibility: Visibility widget cũng có maintainState: truemaintainSize: true/false. Offstage tương đương với Visibility(visible: false, maintainState: true, maintainAnimation: true, maintainSize: false). Nếu em cần kiểm soát chi tiết hơn về việc duy trì kích thước (layout) hay animation, Visibility có thể linh hoạt hơn. Nhưng nếu chỉ đơn giản là "ẩn hoàn toàn nhưng giữ trạng thái", Offstage gọn gàng hơn.

4. Ứng dụng thực tế: Ai đã dùng "sân khấu ẩn" này?

Em cứ nhìn vào các ứng dụng lớn, kiểu gì cũng có bóng dáng của Offstage hoặc các cơ chế tương tự:

  • Các ứng dụng có tab phức tạp: Ví dụ như các ứng dụng ngân hàng, mạng xã hội (Facebook, Zalo) với nhiều tab chính. Khi em chuyển tab, các tab khác thường không bị hủy đi mà chỉ được ẩn đi để khi em quay lại, trạng thái của chúng (cuộn đến đâu, dữ liệu gì đang hiển thị) vẫn còn nguyên.
  • Form nhập liệu nhiều bước/phần: Khi em điền một form dài, có thể có các phần tùy chọn. Thay vì xóa đi và xây lại toàn bộ phần đó, người ta dùng Offstage để ẩn nó đi, giữ nguyên dữ liệu đã nhập.
  • Các công cụ chỉnh sửa ảnh/video: Các panel công cụ, bảng thuộc tính thường được ẩn/hiện linh hoạt. Nếu mỗi lần ẩn đi mà mất hết các thiết lập đang chọn thì phiền toái vô cùng.
  • Game UI: Trong game, các menu, HUD (Head-Up Display) thường được load một lần và sau đó chỉ ẩn/hiện khi cần, để đảm bảo hiệu năng và phản hồi nhanh chóng.

5. Thử nghiệm và khi nào nên dùng Offstage?

Với kinh nghiệm "chinh chiến" của anh Creyt, anh đã dùng Offstage rất nhiều trong các dự án cần sự mượt mà và giữ trạng thái.

Khi nào nên dùng:

  • Toggling nhanh và thường xuyên: Khi em có một phần UI cần ẩn/hiện liên tục (ví dụ: một nút bật/tắt filter, một bảng điều khiển nhỏ).
  • Widget có trạng thái phức tạp hoặc đắt tiền để khởi tạo: Nếu widget của em mất nhiều thời gian để xây dựng hoặc có nhiều logic/dữ liệu cần duy trì (ví dụ: một ListView đã cuộn đến vị trí nhất định, một WebView đã load xong trang), Offstage là vị cứu tinh.
  • Yêu cầu giữ nguyên vị trí trong cây layout (một cách ảo): Mặc dù Offstage không chiếm không gian, nó vẫn giữ widget con trong cây widget để khi hiện ra, nó có thể lấy lại vị trí và context của nó một cách dễ dàng.

Khi nào nên tránh (hoặc cân nhắc giải pháp khác):

  • Widget cực kỳ nặng về bộ nhớ: Nếu widget con của em tiêu thụ quá nhiều RAM ngay cả khi không hiển thị, việc dùng Offstage có thể gây lãng phí tài nguyên. Lúc đó, việc hủy bỏ hoàn toàn widget (dùng if hoặc Visibility với maintainState: false) có thể tốt hơn.
  • Khi em thực sự muốn giải phóng tài nguyên: Nếu widget đó không cần thiết trong một thời gian dài và em muốn hệ thống dọn dẹp nó hoàn toàn, đừng dùng Offstage.
  • Khi cần hiệu ứng chuyển động mượt mà: Offstage chỉ là ẩn/hiện "cộp" một cái. Nếu em muốn có các hiệu ứng mờ dần, trượt vào/ra, thì nên kết hợp với AnimatedOpacity, SlideTransition hoặc Visibility với maintainAnimation: true để đạt được hiệu quả mong muốn.

Tóm lại, Offstage là một công cụ mạnh mẽ trong hộp đồ nghề của developer Flutter, giúp em quản lý trạng thái và tối ưu trải nghiệm người dùng một cách khéo léo. Hãy dùng nó một cách thông minh, và các em sẽ thấy ứng dụng của mình "mượt như bơ" ngay thôi! Chúc các em code vui vẻ!

Thuộc Series: Flutter

Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

#tech #cyberpunk #laravel
Chỉnh sửa bài viết

Bình luận (0)

Vui lòng Đăng Nhập để Bình luận

Hỗ trợ Markdown cơ bản
Nguyễn Văn A
1 ngày trước

Tính năng này đỉnh quá ad ơi, chờ mãi mới thấy một blog Tiếng Việt có UI/UX xịn như vầy!