ViewportOffset: 'Mắt Thần' Của Flutter – Điều Khiển Mọi Cú Lướt!
Flutter

ViewportOffset: 'Mắt Thần' Của Flutter – Điều Khiển Mọi Cú Lướt!

Author

Admin System

@root

Ngày xuất bản

23 Mar, 2026

Lượt xem

1 Lượt

Chào mấy đứa, Creyt đây! Hôm nay mình cùng giải mã một khái niệm nghe thì hàn lâm nhưng thực ra lại là 'trái tim' của mọi cú lướt mượt mà trên app Flutter của mấy đứa: ViewportOffset.

1. ViewportOffset là gì mà 'đỉnh của chóp' vậy?

Tưởng tượng mấy đứa là một đạo diễn phim. Mấy đứa đang quay một cảnh cực dài, nhưng cái máy quay (hay cái viewfinder) của mấy đứa chỉ nhìn được một phần nhỏ của cảnh đó thôi, đúng không? Cái cảnh dài thượt kia chính là nội dung cuộn của mấy đứa (ví dụ, một danh sách sản phẩm dài dằng dặc trên Shopee). Còn cái 'viewfinder' nhỏ bé mà mấy đứa đang nhìn qua, đó chính là cái Viewport – cái cửa sổ nhìn thấy được trên màn hình điện thoại.

Vậy thì, ViewportOffset chính là cái 'tọa độ' mà cái viewfinder của mấy đứa đang đứng trên cái cảnh phim dài đó. Nó cho mấy đứa biết chính xác 'tôi đang nhìn thấy đoạn nào của cái cảnh dài kia, từ điểm nào đến điểm nào'. Nghe hàn lâm hơn, nó là độ lệch của phần nội dung hiển thị (viewport) so với điểm gốc của toàn bộ nội dung cuộn (scrollable content).

Để làm gì? Nó là chìa khóa để Flutter biết được 'Mày đang cuộn đến đâu rồi?' và từ đó render đúng các widget cần thiết. Không có nó, app của mấy đứa sẽ không thể cuộn, không thể biết khi nào cần load thêm dữ liệu (infinite scrolling), hay không thể tạo ra những hiệu ứng parallax 'ảo diệu' khi mấy đứa lướt màn hình.

2. Code Ví Dụ Minh Hoạ: Mở Mắt Thần Ra Xem!

Nói suông thì khó hình dung, giờ mình 'flex' tí code để mấy đứa thấy nó hoạt động như nào nhé. Thường thì mấy đứa sẽ không tương tác trực tiếp với ViewportOffset mà sẽ thông qua ScrollPosition hoặc ScrollController. Nhưng để 'bóc tách' nó ra cho mấy đứa dễ hiểu, mình sẽ dùng NotificationListener để 'nghe lén' các sự kiện cuộn và lấy ra cái offset thần thánh này.

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

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

  @override
  State<ViewportOffsetScreen> createState() => _ViewportOffsetScreenState();
}

class _ViewportOffsetScreenState extends State<ViewportOffsetScreen> {
  double _currentOffset = 0.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ViewportOffset: Mắt Thần Cuộn'),
      ),
      body: NotificationListener<ScrollNotification>(
        onNotification: (ScrollNotification notification) {
          // Chỉ quan tâm đến sự kiện cuộn (ScrollUpdateNotification)
          // hoặc khi cuộn xong (ScrollEndNotification)
          if (notification is ScrollUpdateNotification || notification is ScrollEndNotification) {
            setState(() {
              _currentOffset = notification.metrics.pixels; // Đây chính là ViewportOffset.pixels
            });
          }
          return false; // Trả về false để cho phép các widget khác cũng nhận notification
        },
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Text(
                'Offset hiện tại: ${_currentOffset.toStringAsFixed(2)}',
                style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
            ),
            Expanded(
              child: ListView.builder(
                itemCount: 100, // Danh sách dài dằng dặc
                itemBuilder: (context, index) {
                  return Container(
                    height: 80,
                    margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
                    color: index % 2 == 0 ? Colors.lightBlue[100] : Colors.blue[100],
                    alignment: Alignment.center,
                    child: Text(
                      'Item ${index + 1}',
                      style: const TextStyle(fontSize: 20),
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Trong ví dụ trên, khi mấy đứa cuộn ListView, cái _currentOffset sẽ thay đổi liên tục. Nó chính là giá trị pixels của ScrollMetrics, đại diện cho ViewportOffset – cho mấy đứa biết 'cái viewfinder' đang ở đâu trên 'cảnh phim' dài 100 item kia.

3. Mẹo Hay & Best Practices Từ Creyt

  • Đừng Đụng Trực Tiếp: ViewportOffset là một abstract class, mấy đứa sẽ không bao giờ tạo instance trực tiếp từ nó. Hãy nghĩ nó như một 'khái niệm' hơn là một 'đối tượng cụ thể'. Mấy đứa sẽ tương tác với nó thông qua ScrollPosition (mà ScrollController quản lý) hoặc thông qua ScrollMetrics trong các ScrollNotification.
  • Nghe Lén Là Chính: Để tạo hiệu ứng động hay xử lý logic dựa trên vị trí cuộn, hãy dùng NotificationListener<ScrollNotification> (như ví dụ trên) hoặc ScrollController (để lấy offset qua controller.position.pixels). Đây là cách 'sạch sẽ' nhất để biết app đang cuộn đến đâu.
  • Hiểu Rõ Chiều Dọc/Ngang: offset thường là giá trị dương, tăng dần khi cuộn xuống dưới (hoặc sang phải). Giá trị 0.0 thường là ở đầu danh sách.
  • Giới Hạn Tần Suất: Các sự kiện cuộn xảy ra rất thường xuyên. Nếu mấy đứa thực hiện những tác vụ nặng bên trong onNotification hoặc addListener của ScrollController, hãy cân nhắc dùng throttle hoặc debounce để tối ưu hiệu suất, tránh làm giật lag app.

4. Ứng Dụng Thực Tế: 'Mắt Thần' Đang Ở Đâu?

Mấy đứa có biết các app 'xịn xò' mà mấy đứa dùng hàng ngày đều có bóng dáng của ViewportOffset không?

  • Instagram, Facebook, TikTok: Mấy cái feed cuộn vô tận (infinite scroll) đó, để biết khi nào cần load thêm bài viết mới, app sẽ kiểm tra ViewportOffset để xem người dùng đã cuộn gần đến cuối danh sách chưa.
  • Hiệu Ứng Parallax: Khi mấy đứa cuộn, có những hình ảnh hoặc thành phần UI di chuyển với tốc độ khác nhau, tạo cảm giác chiều sâu. Đó chính là nhờ việc tính toán vị trí của từng phần tử dựa trên ViewportOffset và sau đó áp dụng các phép biến đổi (transform) tương ứng.
  • Sticky Headers/Footers: Các header/footer tự động 'dính' lại ở đầu/cuối màn hình khi cuộn qua một ngưỡng nhất định. ViewportOffset giúp xác định ngưỡng đó.
  • Load Ảnh Lazy Loading: Chỉ tải ảnh khi chúng sắp sửa hoặc đã xuất hiện trong tầm nhìn của người dùng, tiết kiệm băng thông và tăng tốc độ tải trang.

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

Creyt đã từng 'vật lộn' với ViewportOffset nhiều lần, đặc biệt là khi làm mấy cái hiệu ứng UI 'bay bổng' mà designer cứ đòi hỏi. Kinh nghiệm xương máu là:

  • Nên dùng khi:
    • Mấy đứa muốn tạo các hiệu ứng cuộn tùy chỉnh (custom scroll effects) như parallax, zoom khi cuộn.
    • Cần biết chính xác vị trí cuộn để kích hoạt một hành động nào đó (ví dụ: hiển thị nút "Lên đầu trang" khi cuộn xuống một khoảng nhất định).
    • Triển khai lazy loading cho hình ảnh hoặc dữ liệu khi chúng chuẩn bị vào viewport.
    • Xây dựng các indicator cuộn tùy chỉnh (ví dụ: một thanh tiến độ cuộn).
  • Đừng quá lạm dụng: Nếu chỉ cần cuộn đơn giản, ListView.builder hay CustomScrollView đã xử lý 'ngon lành cành đào' rồi, không cần đào sâu vào ViewportOffset chi cho phức tạp. Hãy dùng nó khi mấy đứa cần 'can thiệp' sâu hơn vào hành vi cuộn.
  • Thử nghiệm: Mấy đứa có thể thử thay đổi ScrollNotification thành ScrollStartNotification hay OverscrollNotification để xem các loại sự kiện khác nhau và cách notification.metrics.pixels thay đổi. Hoặc thử dùng ScrollController để animateTo một offset cụ thể. Đó là cách tốt nhất để 'cảm' được nó.

Tóm lại, ViewportOffset chính là 'mắt thần' của Flutter giúp app của mấy đứa 'nhìn' được mình đang cuộn đến đâu. Nắm vững nó, mấy đứa sẽ có thêm một 'siêu năng lực' để làm chủ mọi hiệu ứng cuộn và tạo ra những trải nghiệm người dùng 'mượt như lụa'. Cứ chill mà code thôi mấy đứa!

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!