Một đồng hồ đơn giản, một bài toán thời gian không đơn giản

Đồng hồ đếm ngược nhìn qua chỉ là phép trừ giữa ngày Tết và thời điểm hiện tại. Nhưng ngay khi ứng dụng xuất hiện trên nhiều thiết bị, nhiều múi giờ và qua thời điểm đổi giờ, phép trừ đó có thể cho kết quả khác nhau. Với Sắp Tết, câu hỏi đúng không phải là “còn bao nhiêu mili giây”, mà là “còn bao lâu đến thời khắc giao thừa theo cách người Việt hiểu”. Câu hỏi sản phẩm cần được chuyển thành một quy tắc thời gian rõ ràng trước khi viết mã.

Ngày Tết là một sự kiện gắn với địa phương. Người dùng ở Hà Nội và người đang du lịch tại California vẫn có thể muốn đếm tới cùng một mốc giao thừa Việt Nam. Nếu ứng dụng mặc định lấy múi giờ của thiết bị, hai người sẽ thấy mốc kết thúc khác nhau. Vì vậy, timezone không phải chi tiết triển khai; nó là một phần của định nghĩa sản phẩm và phải xuất hiện trong model dữ liệu.

Chọn một mốc thời gian có chủ đích

Mốc đếm ngược nên được tạo trong vùng Asia/Ho_Chi_Minh, sau đó chuyển thành một instant tuyệt đối để so sánh. Cách làm này tách ý nghĩa địa phương khỏi cơ chế đếm. Nó cũng tránh việc ghép chuỗi ngày rồi để runtime tự đoán múi giờ. Một chuỗi như 2027-02-06T00:00:00 không có offset là dữ liệu chưa hoàn chỉnh; trên hai thiết bị nó có thể đại diện cho hai instant khác nhau.

targetLocal = 2027-02-06 00:00:00
zone = "Asia/Ho_Chi_Minh"
targetInstant = toInstant(targetLocal, zone)
remaining = max(0, targetInstant - clock.now())

Trong production, `clock` nên là dependency có thể thay thế. Test không nên dựa vào đồng hồ thật vì sẽ chậm, không ổn định và khó kiểm tra thời điểm sát giao thừa. Một fixed clock giúp mô phỏng trước mốc, đúng mốc và sau mốc bằng các ca kiểm thử hoàn toàn lặp lại được.

Calendar date khác duration

Đếm ngược thường hiển thị ngày, giờ, phút, giây, nhưng “một ngày” có thể mang hai nghĩa. Duration coi một ngày là 86.400 giây. Calendar date coi ngày là bước chuyển từ một ngày trên lịch sang ngày kế tiếp. Ở Việt Nam hai khái niệm thường trùng nhau vì không có daylight saving time, nhưng người dùng đang ở vùng có DST có thể gặp ngày dài 23 hoặc 25 giờ. Nếu sản phẩm hứa đếm tới instant tại Việt Nam, chia duration tuyệt đối là nhất quán. Nếu hứa “còn bao nhiêu ngày trên lịch”, cần phép tính calendar theo vùng đã chọn.

UI cũng phải nói rõ lựa chọn này. Một nhãn nhỏ “Giờ Việt Nam” loại bỏ nhiều mơ hồ hơn một đoạn code thông minh nhưng vô hình. Khi còn hơn một ngày, ứng dụng có thể ưu tiên số ngày theo duration; khi đến ngày cuối, chuyển sang giờ–phút–giây để tránh cảm giác sai lệch quanh ranh giới lịch.

Đồng bộ mà không làm đồng hồ nhảy

Một countdown cập nhật mỗi giây không nên giả định timer chạy chính xác mỗi 1.000 ms. Hệ điều hành có thể trì hoãn timer khi app ra nền, khi thiết bị tiết kiệm pin hoặc khi main thread bận. Cộng dồn `remaining -= 1` vì thế sẽ trôi dần. Tốt hơn là mỗi nhịp đều lấy `clock.now()` rồi tính lại chênh lệch với target. Timer chỉ quyết định lúc render, không phải nguồn sự thật.

  • Tính lại từ instant ở mỗi tick thay vì giảm một biến đếm.
  • Khi app quay lại foreground, cập nhật ngay trước khi vẽ frame tiếp theo.
  • Dừng timer khi remaining bằng 0 và chuyển sang trạng thái hoàn tất.
  • Không đồng bộ mạng mỗi giây; chỉ hiệu chỉnh clock khi thực sự cần.

Widget và tiến trình nền

Widget có ngân sách cập nhật khác màn hình ứng dụng. Một widget đếm từng giây vừa tốn pin vừa thường không được hệ điều hành đảm bảo. Thiết kế tốt hơn là chọn độ phân giải phù hợp: theo ngày khi còn xa, theo giờ khi đã gần, và mở app cho trải nghiệm đếm chi tiết. Timeline của widget nên chứa các mốc có ý nghĩa thay vì hàng nghìn bản ghi mỗi giây.

Cũng cần quyết định widget hiển thị gì sau giao thừa. Giá trị âm là lỗi trình bày. Trạng thái hoàn tất có thể là lời chúc, mốc năm mới hoặc lời mời khám phá tiện ích Tết khác. Đây là điểm mà state machine rõ ràng — upcoming, imminent, complete — dễ bảo trì hơn nhiều điều kiện rải rác.

Ma trận kiểm thử đáng viết

Các lỗi thời gian thường nằm ở biên. Bộ test hữu ích cần bao phủ một giây trước và sau giao thừa, thiết bị đổi timezone khi app đang chạy, app ngủ qua mốc kết thúc, clock hệ thống bị chỉnh, locale 12/24 giờ và widget bị trì hoãn. Test cả vùng có DST dù mốc nằm ở Việt Nam, vì formatting vẫn chịu ảnh hưởng môi trường người dùng.

given target = TetInstant
for now in [target-1s, target, target+1s]:
  state = countdown(now, target)
  assert state never exposes a negative duration

Điều người dùng nhìn thấy là sự tin cậy

Không ai tải Sắp Tết vì kiến trúc timezone. Họ tải vì muốn cảm nhận thời khắc đang đến gần, và họ chỉ nhận ra phần kỹ thuật khi nó sai. Một định nghĩa thời gian rõ ràng, clock có thể kiểm thử, timer không tích lũy sai số và widget có nhịp cập nhật hợp lý tạo nên cảm giác rất đơn giản ở bề mặt. Đó là dạng đơn giản đáng xây: phần phức tạp được xử lý có chủ đích để người dùng chỉ còn thấy niềm háo hức.