Bạn bọc một Text bằng AnimatedSwitcher, tăng bộ đếm nhưng chỉ thấy chữ thay ngay. Trước khi chỉnh curve hay kéo dài duration, hãy kiểm tra Flutter có nhận đó là child mới hay chỉ cập nhật widget hiện tại.
Bài đối chiếu Flutter API ngày 03/11/2026. Demo được rà thủ công về cây widget, state và hợp đồng key; chưa chạy Flutter SDK, widget test hay thiết bị. Thời lượng 300 ms là cấu hình minh họa, không phải số đo độ mượt hoặc kết quả profiling.
Type và key quyết định cách nhận diện child
Theo tài liệu AnimatedSwitcher, cùng loại widget và cùng key thì tham số mới có thể cập nhật widget hiện có, không tạo transition giữa hai child. Với bộ đếm, đặt ValueKey(count) trên chính Text giúp mỗi giá trị đếm có danh tính mà switcher nhận ra.
Đặt key trên widget bọc sai cấp có thể không thể hiện ý định đó. Cũng không cần tạo UniqueKey mới ở mọi lần build: một rebuild do lý do khác không nên tự bị hiểu là dữ liệu đếm đã thay.
Một demo có đủ state và điểm cập nhật
Có thể đặt vào lib/main.dart của project Material thử:
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: CounterDemo()));
class CounterDemo extends StatefulWidget {
const CounterDemo({super.key});
@override
State<CounterDemo> createState() => _CounterDemoState();
}
class _CounterDemoState extends State<CounterDemo> {
int count = 0;
@override
Widget build(BuildContext context) {
final disableAnimations = MediaQuery.of(context).disableAnimations;
return Scaffold(
appBar: AppBar(title: const Text('Đổi nội dung')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 300),
child: Text(
'$count',
key: ValueKey<int>(count),
style: const TextStyle(fontSize: 48),
),
),
const SizedBox(height: 16),
FilledButton(
onPressed: () => setState(() => count += 1),
child: const Text('Tăng một'),
),
],
),
),
);
}
}
Key lấy từ đúng dữ liệu đang hiển thị và không thay nếu count giữ nguyên. State tăng trong callback của nút, không tăng trong build. Demo dùng transition mặc định; chưa có custom builder hoặc tải dữ liệu bất đồng bộ để đánh giá thêm.
Kiểm tra bằng hai trường hợp đối chứng
Trên project thử, chạy bản có ValueKey<int>(count) rồi tăng chậm từng lần. Sau đó bỏ dòng key và so sánh cùng chuỗi thao tác. Đây là kế hoạch kiểm tra nhận diện child theo tài liệu, chưa phải hai bản build đã được chạy trong bài.
Thử bấm nhanh khi transition cũ chưa xong. AnimatedSwitcher có thể giữ nhiều child đang thoát; đừng giả định cây chỉ có đúng hai Text ở mọi thời điểm. Với nội dung thật dài hoặc có kích thước khác nhau, cần quan sát thêm bố cục trong quá trình chuyển, không chỉ trạng thái cuối.
Xử lý yêu cầu giảm animation theo phạm vi thật
Ví dụ đọc MediaQueryData.disableAnimations và đặt duration bằng 0 khi cờ này bật. Tài liệu hiện tại nêu cờ tương ứng với Remove animations trên Android; iOS reduce motion được cung cấp riêng qua accessibility features, không tự đặt cờ này.
Vì vậy, demo không phải triển khai hỗ trợ giảm chuyển động hoàn chỉnh cho mọi nền tảng. Khi đưa vào app iOS hoặc thiết kế animation riêng, cần đối chiếu tín hiệu và chính sách accessibility tương ứng, rồi kiểm tra trên thiết bị mục tiêu.
Những lỗi dễ gặp khi tích hợp
Key cố định như ValueKey('counter') không phân biệt các số đếm. Key thay đổi ngẫu nhiên lại biến mọi rebuild thành thay child. Cách chọn key nên xuất phát từ điều gì được xem là nội dung mới, không từ mong muốn ép animation chạy bằng mọi cách.
Nếu đổi cả cụm loading, error và content, có thể chọn key biểu diễn trạng thái, nhưng vẫn phải định nghĩa việc cập nhật nội dung trong cùng trạng thái có cần transition hay không. Animation không thay cơ chế quản lý state hoặc đảm bảo dữ liệu mới nhất.
Bắt đầu từ counter để kiểm chứng type, key và duration. Khi ba điều này rõ, bạn mới có cơ sở mở rộng sang nội dung phức tạp và đánh giá chuyển động bằng build thực tế.


