aedev.net
  • Trang chủ
  • AI
  • Mobile
  • Flutter
  • React Native
  • Backend
  • DevOps
  • Hướng dẫn
No Result
View All Result
SAVED POSTS
aedev.net
  • Trang chủ
  • AI
  • Mobile
  • Flutter
  • React Native
  • Backend
  • DevOps
  • Hướng dẫn
No Result
View All Result
aedev.net
No Result
View All Result
Minh họa phát triển ứng dụng mobile đa nền tảng với Flutter

Tìm kiếm Flutter: debounce và chặn response cũ ghi đè kết quả mới

Suong Nguyen by Suong Nguyen
22/10/2026
in Flutter
0
585
SHARES
3.2k
VIEWS
Summarize with ChatGPTShare to Facebook

Người dùng gõ “rea”, sau đó gõ tiếp thành “react”. Request cho chuỗi ngắn lại trả về sau cùng và ghi đè kết quả mới. UI vẫn hoạt động, không có exception, nhưng danh sách không còn ứng với từ khóa đang hiển thị.

Debounce giúp giảm số lần bắt đầu tìm kiếm khi người dùng đang gõ. Nó không bảo đảm thứ tự hoàn tất của những request đã gửi. Bài viết dùng API lõi Dart và Flutter, đối chiếu tài liệu ngày 22/10/2026, không yêu cầu một package debounce riêng.

Ba việc cần xử lý độc lập

  • Hủy Timer cũ khi người dùng tiếp tục thay đổi từ khóa.
  • Bỏ kết quả thuộc một truy vấn đã bị thay thế.
  • Không cập nhật State sau khi widget đã dispose.

Chỉ làm việc đầu tiên chưa đủ. Một Timer đã chạy callback có thể đã tạo request; gọi cancel() trên Timer lúc đó không hủy công việc mạng. Tương tự, mounted chỉ cho biết State còn trong cây widget, không cho biết response có đúng với từ khóa hiện tại hay không.

Tách phần tìm kiếm khỏi giao diện

Truyền hàm tìm kiếm vào widget để ví dụ không bị gắn với HTTP client cụ thể. Hàm nhận từ khóa và trả một Future<List<String>>; trong ứng dụng thật, lớp repository có thể chuyển dữ liệu server thành model trước khi trả cho UI.

import 'dart:async';
import 'package:flutter/material.dart';

class SearchPanel extends StatefulWidget {
  const SearchPanel({super.key, required this.search});
  final Future<List<String>> Function(String query) search;

  @override
  State<SearchPanel> createState() => _SearchPanelState();
}

class _SearchPanelState extends State<SearchPanel> {
  Timer? _timer;
  int _version = 0;
  bool _loading = false;
  String? _error;
  List<String> _items = [];

  void _onChanged(String value) {
    final version = ++_version;
    final query = value.trim();
    _timer?.cancel();
    setState(() {
      _items = [];
      _error = null;
      _loading = query.isNotEmpty;
    });
    if (query.isEmpty) return;
    _timer = Timer(const Duration(milliseconds: 300), () {
      _runSearch(query, version);
    });
  }

  Future<void> _runSearch(String query, int version) async {
    try {
      final items = await widget.search(query);
      if (!mounted || version != _version) return;
      setState(() {
        _items = items;
        _loading = false;
      });
    } catch (_) {
      if (!mounted || version != _version) return;
      setState(() {
        _error = 'Chưa tải được kết quả. Hãy thử lại.';
        _loading = false;
      });
    }
  }

  @override
  void dispose() {
    _timer?.cancel();
    _version++;
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      TextField(
        onChanged: _onChanged,
        decoration: const InputDecoration(labelText: 'Tìm kiếm'),
      ),
      if (_loading) const LinearProgressIndicator(),
      if (_error != null) Text(_error!),
      Expanded(
        child: ListView.builder(
          itemCount: _items.length,
          itemBuilder: (_, index) => ListTile(title: Text(_items[index])),
        ),
      ),
    ]);
  }
}

Đặt widget trong vùng có chiều cao hữu hạn, chẳng hạn Scaffold.body, vì phần danh sách dùng Expanded. Khoảng debounce 300 ms chỉ là lựa chọn minh họa; hãy điều chỉnh theo cách nhập và yêu cầu trải nghiệm của sản phẩm, không xem đó là một con số tối ưu chung.

Vì sao tăng version ngay khi đổi chữ?

Giả sử request A đang chạy. Người dùng vừa gõ từ khóa B nhưng Timer của B chưa hết thời gian chờ. Nếu chỉ đổi version khi bắt đầu request B, response A vẫn có thể lọt qua trong khoảng chờ đó. Tăng version ngay ở onChanged làm A hết hiệu lực ngay khi ý định tìm kiếm thay đổi.

Nhánh lỗi cũng phải kiểm tra version. Nếu một request cũ thất bại sau khi request mới thành công, nó không được phép thay danh sách mới bằng thông báo lỗi. Vì lý do tương tự, một khối finally đặt _loading = false không có điều kiện có thể tắt spinner của truy vấn mới.

Khi người dùng xóa hết từ khóa, version vẫn tăng và Timer bị hủy. Kết quả đang bay về vì thế không tự làm danh sách xuất hiện lại. Bạn có thể thay UI rỗng bằng hướng dẫn “Nhập từ khóa” hoặc lịch sử tìm kiếm tùy sản phẩm; đó là quyết định trình bày riêng.

Hủy kết quả khác với hủy request

Mẫu này bỏ qua response không còn liên quan, nhưng không dừng lưu lượng mạng đã bắt đầu. Nếu HTTP client hỗ trợ hủy hoặc repository quản lý tác vụ đang chạy, có thể thêm cơ chế hủy ở lớp đó. Vẫn nên giữ version check, vì yêu cầu hủy và callback hoàn tất có thể ở gần nhau.

Nếu đổi repository hoặc bộ lọc tìm kiếm khi widget vẫn được giữ lại, cần vô hiệu hóa truy vấn thuộc cấu hình trước. Có thể xử lý ở didUpdateWidget hoặc thiết kế parent tạo key mới phù hợp. Không nên ngầm giả định mọi thay đổi điều kiện tìm kiếm đều đi qua TextField.

Kiểm tra với thứ tự trả về có chủ đích

Trong widget test, thay repository bằng các Completer<List<String>> để kiểm soát thời điểm hoàn tất. Gõ A rồi B, hoàn tất B trước A và quan sát chỉ B được giữ. Thử tiếp lỗi của A, xóa từ khóa khi đang chờ, rồi đóng màn hình trước khi Future hoàn tất.

Đây là kế hoạch kiểm thử cần chạy trên dự án, không phải khẳng định rằng snippet đã được chạy trên thiết bị Android/iOS. Bài FutureBuilder và vòng đời Future bổ sung góc nhìn về nơi tạo tác vụ; bài hiện tại tập trung vào tính hợp lệ của response khi có nhiều truy vấn.

Kết luận

Debounce kiểm soát thời điểm bắt đầu, version kiểm soát kết quả còn hợp lệ và mounted bảo vệ vòng đời widget. Xử lý rõ ba ranh giới này giúp tránh một loại lỗi tìm kiếm khó thấy khi chỉ thử với mạng nhanh.

Nguồn tham khảo

  • Dart: Timer và cancel
  • Flutter: State.mounted

Tags: AsyncDartFlutter
SummarizeShare234
Suong Nguyen

Suong Nguyen

Related Stories

Minh họa phát triển ứng dụng mobile đa nền tảng với Flutter

FutureBuilder trong Flutter: đừng tạo Future mới trong build

by Suong Nguyen
21/10/2026
0

Giữ Future ổn định trong State, hiển thị loading, lỗi và dữ liệu rỗng, đồng thời chỉ tải lại khi người dùng yêu cầu.

Minh họa phát triển ứng dụng mobile đa nền tảng với Flutter

Bắt đầu với Flutter: khi nào nên chọn cho dự án mobile?

by admin
02/07/2026
0

Flutter là lựa chọn đáng cân nhắc khi cần phát triển ứng dụng iOS và Android với một codebase, UI nhất quán và tốc độ ra sản phẩm...

Minh họa so sánh React Native và Flutter cho phát triển mobile

React Native hay Flutter: chọn gì cho sản phẩm mới?

by admin
02/07/2026
0

React Native và Flutter đều mạnh cho mobile cross-platform, nhưng phù hợp với những kiểu team và sản phẩm khác nhau.

0 0 votes
Article Rating
Subscribe
Login
Notify of
guest
guest
0 Comments
Oldest
Newest Most Voted
AE Dev

AE Dev mang đến các bài viết về lập trình, mobile app, backend, frontend, WordPress và công nghệ, phù hợp cho developer muốn học hỏi và cập nhật kiến thức thực tế.

Recent Posts

  • React Native: tránh bàn phím che form với KeyboardAvoidingView và ScrollView
  • Phỏng vấn JavaScript: giải thích event loop, microtask và async/await
  • Deep link mobile an toàn: đúng domain chưa đủ, còn route và quyền đọc

Categories

  • AI
  • Backend
  • Đánh giá
  • DevOps
  • Flutter
  • Hướng dẫn
  • Lập trình Mobile
  • Phỏng vấn
  • React Native
  • Uncategorized

Weekly Newsletter

  • Buy JNews
  • Support Forum
  • Pre-sale Question
  • Contact Us

© 2026 AE Dev - Blog công nghệ, lập trình và chia sẻ kiến thức IT.

Welcome Back!

Login to your account below

Forgotten Password?

Retrieve your password

Please enter your username or email address to reset your password.

Log In
No Result
View All Result
  • Trang chủ
  • AI
  • Mobile
  • React Native
  • Backend
  • DevOps
  • Hướng dẫn
  • Flutter

© 2026 AE Dev - Blog công nghệ, lập trình và chia sẻ kiến thức IT.

wpDiscuz
0
0
Would love your thoughts, please comment.x
()
x
| Reply