NestedScrollViewState: Bậc thầy điều khiển cuộn cuộn trong Flutter!
Flutter

NestedScrollViewState: Bậc thầy điều khiển cuộn cuộn trong Flutter!

Author

Admin System

@root

Ngày xuất bản

20 Mar, 2026

Lượt xem

2 Lượt

"NestedScrollViewState"

Yo, fam! Đã bao giờ bạn lướt TikTok, Instagram hay YouTube và thấy mấy cái app đó có cái header (thanh tiêu đề) nó kiểu "biến hình" cực mượt chưa? Lúc thì to đùng, lúc lại co lại tí hon khi bạn cuộn nội dung? Đó không phải là phép thuật đâu nhá, mà là công nghệ! Và trong Flutter, cái "phép thuật" đó phần lớn đến từ một combo siêu đỉnh: NestedScrollView và "linh hồn" của nó, NestedScrollViewState.

1. NestedScrollViewState: "Conductor" của Dàn nhạc Cuộn Cuộn

Nói một cách Gen Z cho dễ hình dung: NestedScrollView giống như một cái "hộp cuộn" đa năng, cho phép bạn nhét nhiều cái "hộp cuộn con" khác vào trong, nhưng tất cả chúng lại biết cách làm việc nhóm với nhau. Ví dụ, bạn có một cái header (thanh tiêu đề) có thể co giãn, bên dưới là một danh sách sản phẩm dài dằng dặc cũng cuộn được. NestedScrollView sẽ đảm bảo khi bạn cuộn danh sách sản phẩm lên, cái header kia cũng tự động "nghe lời" mà co lại.

Vậy còn NestedScrollViewState? Nó chính là "ông bầu" hay "conductor" tài ba của dàn nhạc cuộn cuộn này. NestedScrollViewState không phải là widget bạn dùng trực tiếp để xây dựng UI, mà nó là cái "trạng thái" nội bộ, cái "bộ não" điều khiển cách mà NestedScrollView hoạt động. Nó nắm giữ thông tin về trạng thái cuộn của cả phần header bên ngoài và phần nội dung bên trong, giúp chúng ta can thiệp, điều khiển hoặc lắng nghe các sự kiện cuộn một cách "chủ động" hơn.

Tóm lại: NestedScrollView là cái khung cảnh sân khấu, còn NestedScrollViewState là người đạo diễn đứng sau cánh gà, điều phối mọi chuyển động cuộn một cách mượt mà và ăn khớp.

2. Code Ví Dụ Minh Họa: Màn "Biến Hình" của Header

Để dễ hiểu, chúng ta sẽ làm một ví dụ kinh điển: một trang có SliverAppBar co giãn và một TabBarView với các tab chứa danh sách riêng biệt. NestedScrollViewState sẽ giúp chúng ta "nói chuyện" với các ScrollController của cả phần header và phần body.

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: 'NestedScrollView Demo by Creyt',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const NestedScrollViewPage(),
    );
  }
}

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

  @override
  State<NestedScrollViewPage> createState() => _NestedScrollViewPageState();
}

class _NestedScrollViewPageState extends State<NestedScrollViewPage> with SingleTickerProviderStateMixin {
  late TabController _tabController;
  // Key để truy cập NestedScrollViewState
  final GlobalKey<NestedScrollViewState> _nestedScrollViewKey = GlobalKey();

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
    // Lắng nghe sự kiện cuộn của NestedScrollView
    // Đây là cách bạn có thể tương tác với NestedScrollViewState
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _nestedScrollViewKey.currentState?.outerController.addListener(() {
        // Ví dụ: in ra vị trí cuộn của header
        // print('Outer Scroll Offset: ${_nestedScrollViewKey.currentState?.outerController.offset}');
      });
    });
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        key: _nestedScrollViewKey, // Gắn key vào NestedScrollView
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
          return <Widget>[
            SliverAppBar(
              title: const Text('Creyt\'s Nested Scroll'),
              floating: true, // Header sẽ nổi lên khi cuộn xuống một chút
              pinned: true,   // Header sẽ luôn ghim lại ở top khi co lại hết cỡ
              snap: true,     // Kết hợp với floating, giúp header hiện lên nhanh hơn
              expandedHeight: 200.0, // Chiều cao ban đầu của header
              flexibleSpace: FlexibleSpaceBar(
                centerTitle: true,
                title: innerBoxIsScrolled
                    ? null // Khi cuộn vào, title mặc định của SliverAppBar sẽ hiện
                    : const Text('Chào Mừng Gen Z!', style: TextStyle(color: Colors.white, fontSize: 20)),
                background: Image.network(
                  'https://picsum.photos/800/400?random=1', // Ảnh nền đẹp zai
                  fit: BoxFit.cover,
                ),
              ),
              bottom: TabBar(
                controller: _tabController,
                tabs: const <Widget>[
                  Tab(text: 'Tab 1'),
                  Tab(text: 'Tab 2'),
                  Tab(text: 'Tab 3'),
                ],
              ),
            ),
          ];
        },
        body: TabBarView(
          controller: _tabController,
          children: <Widget>[
            _buildTabContent('Nội dung Tab 1'),
            _buildTabContent('Nội dung Tab 2'),
            _buildTabContent('Nội dung Tab 3'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // Sử dụng NestedScrollViewState để cuộn lên đầu
          // outerController là ScrollController của phần header
          _nestedScrollViewKey.currentState?.outerController.animateTo(
            0.0,
            duration: const Duration(milliseconds: 500),
            curve: Curves.easeInOut,
          );
        },
        child: const Icon(Icons.arrow_upward),
      ),
    );
  }

  Widget _buildTabContent(String title) {
    return ListView.builder(
      // Quan trọng: ListView trong NestedScrollView không cần ScrollController riêng
      // NestedScrollView sẽ tự động quản lý nó.
      itemCount: 50,
      itemBuilder: (BuildContext context, int index) {
        return Card(
          margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Text('$title - Item ${index + 1}', style: const TextStyle(fontSize: 16)),
          ),
        );
      },
    );
  }
}

Trong ví dụ trên:

Gợi Ý Đọc Tiếp
Backdrop trong Flutter: Sân Khấu UI Đa Chiều

4 Lượt xem

  • Chúng ta dùng GlobalKey<NestedScrollViewState> để có thể "nắm thóp" được NestedScrollViewState từ bên ngoài. Đây là cách để bạn tương tác trực tiếp với nó.
  • _nestedScrollViewKey.currentState?.outerController cho phép bạn truy cập ScrollController của phần header. Từ đó, bạn có thể addListener để theo dõi vị trí cuộn, hoặc animateTo để cuộn lên/xuống theo ý muốn (như nút FloatingActionButton đã làm).
Illustration

3. Mẹo (Best Practices) từ "Lão Làng" Creyt

  • Đừng lạm dụng! NestedScrollView mạnh thật, nhưng đừng dùng nó cho mọi thứ. Nếu bạn chỉ cần một danh sách cuộn đơn giản, ListView hoặc CustomScrollView (với các sliver đơn giản) là đủ rồi. Dùng đúng chỗ mới là pro.
  • Hiểu "linh hồn" của nó: NestedScrollView có hai phần chính: headerSliverBuilder (cho các phần header co giãn) và body (cho nội dung cuộn bên trong). Hãy tưởng tượng headerSliverBuilder là cái "áo khoác" và body là "người" mặc áo. Khi "người" di chuyển, "áo khoác" cũng phải điều chỉnh theo.
  • ScrollController là chìa khóa: Nếu bạn muốn làm những trò "ảo diệu" như tự động cuộn, lắng nghe sự kiện cuộn, hay đồng bộ cuộn giữa nhiều phần, hãy nắm lấy outerControllerinnerController thông qua NestedScrollViewState. Nhớ nhé, NestedScrollView sẽ tự động cung cấp ScrollController cho body của nó, bạn không cần tự tạo thêm nữa.
  • GlobalKey là "cầu nối": Khi bạn cần truy cập NestedScrollViewState từ một widget cha hoặc từ một FloatingActionButton như ví dụ, GlobalKey là người bạn thân thiết nhất. Nó cho phép bạn "gọi tên" và "nói chuyện" với state của widget đó.
  • Performance: Tránh xây dựng những Sliver quá phức tạp hoặc danh sách quá dài trong headerSliverBuilder hoặc body mà không dùng builder (như SliverList.builder, ListView.builder). Hiệu suất là vàng, đặc biệt trên mobile.

4. Ứng Dụng Thực Tế: "Thấy quen mà không biết tên"

Bạn đã thấy NestedScrollView "tung hoành" ở khắp mọi nơi rồi đấy:

  • Trang cá nhân Instagram/Facebook: Phần thông tin cá nhân (ảnh đại diện, số lượng follower) ở trên sẽ co lại khi bạn cuộn xuống xem feed bài viết.
  • Ứng dụng YouTube: Khi bạn xem video, phần video player ở trên sẽ co nhỏ lại khi bạn cuộn xuống xem bình luận hoặc video gợi ý.
  • Google Play Store/App Store: Trang chi tiết ứng dụng, phần ảnh bìa và thông tin cơ bản sẽ co lại khi bạn cuộn xuống đọc mô tả, đánh giá.
  • Bất kỳ ứng dụng nào có SliverAppBar "ngầu lòi": Đấy, chính nó đấy!

5. Thử Nghiệm và Nên Dùng Cho Case Nào

Anh Creyt đã từng "đau đầu" với việc làm sao cho mấy cái header nó "nhảy múa" đúng ý. Hồi xưa, chưa có NestedScrollView, phải tự "chế" bằng tay, tính toán offset các kiểu con đà điểu, cực lắm! Giờ có NestedScrollView rồi thì mọi thứ dễ thở hơn nhiều.

Nên dùng NestedScrollView khi:

  • Bạn muốn một SliverAppBar có thể co giãn (expand/collapse) và đồng bộ với việc cuộn của nội dung bên dưới.
  • Bạn có một TabBar nằm trong SliverAppBar và muốn các nội dung của TabBarView cuộn "chung nhịp" với SliverAppBar.
  • Bạn cần một hiệu ứng cuộn "phức tạp" hơn, nơi mà một phần giao diện (header) sẽ thay đổi kích thước hoặc vị trí dựa trên hành vi cuộn của một phần giao diện khác (body).
  • Bạn muốn có quyền kiểm soát ScrollController của cả phần header (outerController) và phần body (innerController) để làm những logic tùy chỉnh.

Không nên dùng NestedScrollView khi:

  • Bạn chỉ có một danh sách đơn giản và không cần header co giãn.
  • Bạn cần hai danh sách cuộn độc lập hoàn toàn với nhau (không có sự tương tác giữa chúng).

Nhớ nhé, NestedScrollViewState không phải là thứ bạn "nhìn thấy" trực tiếp, mà nó là "bộ não" điều khiển cái trải nghiệm cuộn mượt mà mà bạn "cảm nhận" được. Nắm vững nó, bạn sẽ tạo ra những UI "đỉnh của chóp" trong Flutter!

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!