20 thg 4, 2025·8 phút đọc

Mạng Kotlin cho kết nối chậm: thời gian chờ và thử lại an toàn

Thực tiễn mạng Kotlin cho kết nối chậm: đặt thời gian chờ phù hợp, cache an toàn, retry không sinh trùng lặp, và bảo vệ các hành động quan trọng trên mạng di động không ổn định.

Mạng Kotlin cho kết nối chậm: thời gian chờ và thử lại an toàn

Điều gì hỏng trên kết nối chậm và không ổn định

Trên di động, “chậm” thường không có nghĩa là “không có internet”. Thường đó là kết nối vẫn hoạt động nhưng chỉ trong những đợt ngắn. Một yêu cầu có thể mất 8 đến 20 giây, dừng giữa chừng rồi hoàn tất. Hoặc nó có thể thành công lúc này và thất bại lúc khác vì điện thoại chuyển từ Wi‑Fi sang LTE, vào khu vực tín hiệu yếu, hoặc OS đưa app vào nền.

“Không ổn định” còn tệ hơn. Gói tin rớt, lookup DNS timeout, TLS handshake thất bại, và kết nối bị reset ngẫu nhiên. Bạn có thể làm mọi thứ “đúng” trong mã và vẫn thấy lỗi thực tế vì mạng thay đổi bên dưới.

Đây là lúc các thiết lập mặc định thường vỡ. Nhiều app dựa vào mặc định của thư viện cho timeout, retry và cache mà không quyết định thế nào là “đủ tốt” cho người dùng thật. Mặc định thường được tinh chỉnh cho Wi‑Fi ổn định và API nhanh, không phải cho tàu commute, thang máy, hay quán cà phê đông người.

Người dùng không mô tả “socket timeout” hay “HTTP 503”. Họ nhận ra triệu chứng: spinner vô tận, lỗi đột ngột sau khi chờ lâu (rồi lần sau thì thành công), hành động trùng lặp (hai đặt chỗ, hai đơn hàng, trừ tiền hai lần), cập nhật bị mất, và trạng thái lẫn lộn khi UI báo “thất bại” nhưng server thực ra đã thành công.

Mạng chậm biến các kẽ hở nhỏ trong thiết kế thành vấn đề tiền bạc và niềm tin. Nếu app không tách rõ “vẫn đang gửi” với “thất bại” và “xong”, người dùng sẽ chạm lại. Nếu client retry một cách mù quáng, nó có thể tạo ra bản sao. Nếu server không hỗ trợ idempotency, một kết nối lung lay có thể tạo nhiều ghi “thành công”.

“Hành động quan trọng” là bất cứ thứ gì phải xảy ra nhiều nhất một lần và phải chính xác: thanh toán, gửi checkout, đặt chỗ, chuyển điểm, đổi mật khẩu, lưu địa chỉ giao hàng, gửi khiếu nại, hay gửi phê duyệt.

Một ví dụ thực tế: ai đó gửi checkout khi LTE yếu. App gửi yêu cầu, rồi kết nối rớt trước khi phản hồi tới. Người dùng thấy lỗi, chạm “Pay” lần nữa, và giờ hai yêu cầu tới server. Nếu không có quy tắc rõ ràng, app không biết nên retry, đợi hay dừng. Người dùng cũng không biết có nên thử lại hay không.

Quyết định quy tắc trước khi điều chỉnh mã

Khi kết nối chậm hoặc không ổn định, hầu hết lỗi đến từ quy tắc không rõ ràng, chứ không phải từ client HTTP. Trước khi động tới timeout, cache hay retry, hãy viết ra “đúng” nghĩa là gì cho app của bạn.

Bắt đầu với các hành động không được phép chạy hai lần. Thường là hành động liên quan tiền và tài khoản: đặt hàng, trừ thẻ, gửi payout, đổi mật khẩu, xoá tài khoản. Nếu người dùng chạm hai lần hoặc app retry, server vẫn phải xử lý như một yêu cầu. Nếu chưa đảm bảo được, coi những endpoint đó là “không tự động retry” cho tới khi bạn làm được.

Tiếp theo, quyết định màn hình nào được phép làm gì khi mạng xấu. Một số màn hình vẫn hữu ích khi offline (hồ sơ gần nhất, đơn hàng trước). Màn hình khác nên chuyển thành chỉ đọc hoặc hiển thị trạng thái “thử lại” rõ ràng (tồn kho, giá trực tiếp). Trộn lẫn những kỳ vọng này dẫn đến UI rối và cache rủi ro.

Đặt thời gian chờ chấp nhận được cho mỗi hành động theo cách người dùng nghĩ, không phải theo cảm nhận hay gọn trong mã. Đăng nhập chịu đựng chờ ngắn. Upload file cần lâu hơn. Checkout cần cảm giác nhanh nhưng cũng an toàn. Timeout 30 giây có thể “đáng tin” trên giấy nhưng vẫn khiến trải nghiệm tệ.

Cuối cùng, quyết định bạn sẽ lưu gì trên thiết bị và trong bao lâu. Cache hữu ích, nhưng dữ liệu cũ có thể dẫn tới quyết định sai (giá cũ, quyền hết hạn).

Viết các quy tắc ở nơi mọi người dễ tìm (README là ổn). Giữ đơn giản:

  • Endpoint nào là “không được trùng” và cần xử lý idempotency?
  • Màn hình nào phải hoạt động offline, màn hình nào chỉ đọc khi offline?
  • Thời gian chờ tối đa cho mỗi hành động (login, refresh feed, upload, checkout)?
  • Cái gì được cache trên thiết bị, và expiry là bao lâu?
  • Sau thất bại, bạn hiển thị lỗi, xếp hàng đợi để gửi sau, hay yêu cầu người dùng thử lại?

Khi các quy tắc rõ ràng, giá trị timeout, header cache, chính sách retry và trạng thái UI sẽ dễ triển khai và test hơn.

Thời gian chờ phù hợp với kỳ vọng người dùng

Mạng chậm hỏng theo nhiều cách khác nhau. Một cấu hình timeout tốt không chỉ “chọn một con số”. Nó phải phản ánh người dùng đang làm gì và fail đủ nhanh để app có thể phục hồi.

Ba loại timeout, nói dễ hiểu:

  • Connect timeout: chờ bao lâu để thiết lập kết nối tới server (DNS, TCP, TLS). Nếu thất bại, yêu cầu chưa thực sự bắt đầu.
  • Write timeout: chờ bao lâu trong khi gửi thân yêu cầu (upload, JSON lớn, uplink chậm).
  • Read timeout: chờ server gửi dữ liệu trả về sau khi yêu cầu đã được gửi. Thường xuất hiện trên mạng di động bị chập chờn.

Timeout nên phản ánh màn hình và mức độ quan trọng. Feed có thể chậm hơn mà không gây hại lớn. Hành động quan trọng nên hoàn thành hoặc fail rõ ràng để người dùng biết nên làm gì tiếp theo.

Một điểm khởi đầu thực tế (điều chỉnh sau khi đo):

  • Tải danh sách (rủi ro thấp): connect 5–10s, read 20–30s, write 10–15s.
  • Tìm kiếm khi gõ: connect 3–5s, read 5–10s, write 5–10s.
  • Hành động quan trọng (rủi ro cao, như “Pay” hoặc “Submit order”): connect 5–10s, read 30–60s, write 15–30s.

Tính nhất quán quan trọng hơn hoàn hảo. Nếu người dùng chạm “Submit” và thấy spinner hai phút, họ sẽ chạm lại.

Tránh “loading vô hạn” bằng cách thêm ngưỡng tối đa trong UI. Hiển thị tiến độ ngay, cho phép huỷ, và sau (ví dụ) 20–30 giây hiển thị “Vẫn đang cố…” với các tuỳ chọn thử lại hoặc kiểm tra kết nối. Điều đó giữ trải nghiệm trung thực ngay cả khi thư viện mạng vẫn đang chờ.

Khi timeout xảy ra, ghi log đủ để gỡ lỗi sau này, nhưng tránh log bí mật. Các trường hữu ích gồm đường dẫn URL (không phải query đầy đủ), method HTTP, status (nếu có), phân tách thời gian (connect vs write vs read nếu có), loại mạng (Wi‑Fi, di động, airplane mode), kích thước ước tính request/response, và một request ID để khớp log client với log server.

Một thiết lập mạng Kotlin đơn giản và nhất quán

Khi kết nối chậm, những khác biệt nhỏ trong cấu hình client trở thành vấn đề lớn. Một baseline sạch sẽ giúp bạn gỡ lỗi nhanh hơn và đảm bảo mọi yêu cầu theo cùng quy tắc.

Một client, một chính sách

Bắt đầu với một chỗ duy nhất nơi bạn xây dựng HTTP client (thường là một OkHttpClient dùng bởi Retrofit). Đặt những thứ cơ bản ở đó để mọi yêu cầu hành xử giống nhau: header mặc định (phiên bản app, locale, token auth) và User‑Agent rõ ràng, timeout đặt một lần (không rải rác khắp nơi), logging bật tắt được, và một quyết định retry duy nhất (dù là “không tự động retry”).

Dưới đây là ví dụ nhỏ giữ cấu hình trong một file:

val okHttp = OkHttpClient.Builder()
  .connectTimeout(10, TimeUnit.SECONDS)
  .readTimeout(20, TimeUnit.SECONDS)
  .writeTimeout(20, TimeUnit.SECONDS)
  .callTimeout(30, TimeUnit.SECONDS)
  .addInterceptor { chain ->
    val request = chain.request().newBuilder()
      .header("User-Agent", "MyApp/${BuildConfig.VERSION_NAME}")
      .header("Accept", "application/json")
      .build()
    chain.proceed(request)
  }
  .build()

val retrofit = Retrofit.Builder()
  .baseUrl(BASE_URL)
  .client(okHttp)
  .addConverterFactory(MoshiConverterFactory.create())
  .build()

Xử lý lỗi tập trung và ánh xạ sang thông điệp người dùng

Lỗi mạng không chỉ là “một ngoại lệ”. Nếu mỗi màn hình xử lý khác nhau, người dùng sẽ thấy thông báo loạn. Tạo một mapper chuyển các thất bại thành một số kết quả thân thiện với người dùng: không có kết nối/airplane mode, timeout, lỗi server (5xx), lỗi validation hoặc auth (4xx), và fallback không xác định.

Điều này giữ copy UI nhất quán (“Không có kết nối” vs “Thử lại”) mà không tiết lộ chi tiết kỹ thuật.

Gắn tag và huỷ yêu cầu khi màn hình đóng

Trên mạng không ổn định, các call có thể hoàn tất muộn và cập nhật màn hình đã mất. Hãy coi huỷ là quy tắc chuẩn: khi màn hình đóng, công việc của nó dừng.

Với Retrofit và Kotlin coroutines, huỷ coroutine scope (ví dụ trong ViewModel) sẽ huỷ call HTTP tương ứng. Với các call không dùng coroutine, giữ tham chiếu tới Call và gọi cancel(). Bạn cũng có thể gắn tag cho yêu cầu và huỷ nhóm call khi tính năng bị thoát.

Công việc nền không nên phụ thuộc vào UI

Bất cứ việc quan trọng nào phải hoàn thành (gửi báo cáo, sync hàng đợi, hoàn tất một submission) nên chạy trong scheduler thiết kế cho việc đó. Trên Android, WorkManager thường là lựa chọn vì nó có thể retry sau và tồn tại qua khởi động lại app. Giữ hành động UI nhẹ, và giao các công việc dài cho job nền khi hợp lý.

Quy tắc cache an toàn trên di động

Create reliable internal tools
Build internal tools and admin panels that still work when the network is unreliable.
See platform

Cache có thể là lợi thế lớn trên kết nối chậm vì giảm tải download lặp lại và làm màn hình cảm thấy tức thì. Nó cũng có thể gây vấn đề nếu hiển thị dữ liệu cũ vào thời điểm sai, như số dư tài khoản cũ hay địa chỉ giao hàng đã hết hạn.

Cách tiếp cận an toàn là chỉ cache những gì người dùng chấp nhận được khi hơi cũ, và buộc kiểm tra tươi cho mọi thứ ảnh hưởng tới tiền, bảo mật, hoặc quyết định cuối cùng.

Những header Cache‑Control cơ bản bạn có thể tin tưởng

Hầu hết quy tắc về vài header:

  • max-age=60: bạn có thể tái sử dụng response cache trong 60 giây mà không hỏi server.
  • no-store: đừng lưu response này (tốt cho token và màn nhạy cảm).
  • must-revalidate: nếu đã hết hạn, bạn phải kiểm tra với server trước khi dùng lại.

Trên di động, must-revalidate ngăn dữ liệu “sai lặng lẽ” sau một khoảng offline tạm thời. Nếu người dùng mở app sau chuyến tàu, bạn muốn màn hình nhanh, nhưng cũng muốn app xác nhận điều gì vẫn đúng.

Làm mới bằng ETag: nhanh, rẻ và đáng tin

Với các endpoint đọc, xác thực bằng ETag thường tốt hơn max-age dài. Server gửi ETag cùng response. Lần sau, app gửi If-None-Match với giá trị đó. Nếu không thay đổi, server trả 304 Not Modified, nhỏ gọn và nhanh trên mạng yếu.

Cách này phù hợp cho danh sách sản phẩm, chi tiết hồ sơ, và màn cài đặt.

Quy tắc đơn giản:

  • Cache endpoint “read” với max-age ngắn kèm must-revalidate, và hỗ trợ ETag khi có thể.
  • Không cache endpoint “write” (POST/PUT/PATCH/DELETE). Xử lý chúng luôn phụ thuộc mạng.
  • Dùng no-store cho mọi thứ nhạy cảm (auth, bước thanh toán, tin nhắn riêng tư).
  • Cache tài nguyên tĩnh (icon, cấu hình công khai) lâu hơn vì rủi ro dữ liệu lỗi thấp.

Giữ quyết định cache nhất quán khắp app. Người dùng dễ nhận ra sự không khớp hơn là vài giây chậm.

Thử lại an toàn mà không làm tồi thêm tình hình

Own the source code
Get real source code you can export, review, and deploy where you need.
Generate code

Retry nghe có vẻ dễ, nhưng có thể phản tác dụng. Retry sai yêu cầu sẽ tạo thêm tải, ngốn pin, và khiến app có cảm giác mắc kẹt. Chỉ retry những lỗi có khả năng tạm thời thành công. Kết nối rớt, timeout, hoặc outage ngắn có thể thành công khi thử lại. Mật khẩu sai, thiếu trường, hay 404 thì không.

Quy tắc thực tế:

  • Retry các timeout và lỗi kết nối.
  • Retry 502, 503, và đôi khi 504.
  • Không retry 4xx (trừ 408 hoặc 429 nếu bạn có quy tắc chờ rõ ràng).
  • Không retry những yêu cầu đã tới server và có thể đang xử lý.
  • Giữ số lần retry thấp (thường 1–3 lần).

Backoff + jitter: giảm bão retry

Nếu nhiều người dùng gặp cùng outage, retry đồng loạt có thể tạo sóng tải làm chậm phục hồi. Dùng exponential backoff (chờ lâu hơn mỗi lần) và thêm jitter (độ trễ ngẫu nhiên nhỏ) để thiết bị không retry đồng bộ.

Ví dụ: chờ khoảng 0.5s, rồi 1s, rồi 2s, kèm ±20% ngẫu nhiên mỗi lần.

Giới hạn tổng thời gian retry

Nếu không có giới hạn, retry có thể giữ người dùng trong spinner cả chục phút. Chọn thời gian tối đa cho toàn bộ thao tác, bao gồm mọi chờ. Nhiều app nhắm 10–20 giây trước khi dừng và hiển thị tuỳ chọn thử lại rõ ràng.

Cũng hãy phù hợp với ngữ cảnh. Nếu ai đó submit form, họ muốn câu trả lời nhanh. Nếu sync nền thất bại, bạn có thể retry sau.

Không bao giờ auto‑retry các hành động không idempotent (như đặt hàng, gửi thanh toán) trừ khi bạn có bảo vệ như idempotency key hoặc kiểm tra trùng lặp server side. Nếu bạn không chắc an toàn, hãy fail rõ ràng và để người dùng quyết định.

Ngăn trùng lặp cho các hành động quan trọng

Trên kết nối chậm hoặc không ổn định, người dùng chạm hai lần. OS có thể retry ngầm. App có thể gửi lại sau timeout. Nếu hành động là “tạo cái gì đó” (đặt hàng, gửi tiền, đổi mật khẩu), trùng lặp gây hại.

Idempotency nghĩa là cùng một yêu cầu sẽ tạo cùng một kết quả. Nếu lặp lại, server không nên tạo đơn hàng thứ hai; nó nên trả lại kết quả đầu tiên hoặc báo “đã xong”.

Dùng idempotency key cho mỗi lần thử quan trọng

Với hành động quan trọng, tạo một idempotency key duy nhất khi người dùng bắt đầu và gửi nó kèm yêu cầu (thường là header Idempotency-Key hoặc một trường trong body).

Luồng thực tế:

  • Tạo UUID idempotency key khi người dùng chạm “Pay”.
  • Lưu cục bộ một bản nhỏ: status = pending, createdAt, hash payload yêu cầu.
  • Gửi yêu cầu kèm key.
  • Khi nhận thành công, đánh dấu status = done và lưu ID kết quả từ server.
  • Nếu cần retry, tái sử dụng cùng key, không tạo key mới.

Quy tắc “tái sử dụng cùng key” là thứ ngăn chặn việc trừ tiền hai lần.

Xử lý khởi động lại app và khoảng offline

Nếu app bị kill giữa chừng, lần mở tiếp theo vẫn phải an toàn. Lưu idempotency key và trạng thái yêu cầu vào storage cục bộ (ví dụ một dòng nhỏ trong database). Khi khởi động lại, hoặc retry bằng cùng key, hoặc gọi endpoint “check status” dùng key hoặc ID kết quả đã lưu.

Phía server, hợp đồng nên rõ ràng: khi nhận key trùng, nó nên từ chối lần thứ hai hoặc trả lại response ban đầu (cùng order ID, cùng receipt). Nếu server chưa làm được, ngăn trùng lặp client sẽ không bao giờ hoàn toàn tin cậy, vì app không thể biết chuyện gì đã xảy ra sau khi gửi yêu cầu.

Một chạm thân thiện với người dùng: nếu một lần thử đang pending, hiện “Payment in progress” và khoá nút cho tới khi có kết quả cuối cùng.

Mẫu UI giảm gửi lại nhầm lẫn

Build for flaky networks
Build a mobile app and backend with clear retry and timeout rules from day one.
Try AppMaster

Kết nối chậm không chỉ làm hỏng yêu cầu. Nó thay đổi cách người ta chạm. Khi màn hình đứng 2 giây, nhiều người nghĩ không có gì xảy ra và bấm lại. UI phải làm cho “một lần chạm” cảm thấy đáng tin ngay cả khi mạng xấu.

Optimistic UI an toàn khi hành động có thể đảo lại hoặc rủi ro thấp, như đánh dấu sao, lưu nháp, hay đánh dấu đã đọc. Với tiền, tồn kho, xóa không thể hoàn tác, dùng Confirmed UI.

Mặc định tốt cho hành động quan trọng là trạng thái pending rõ ràng. Sau lần chạm đầu, ngay lập tức chuyển nút chính thành “Submitting…”, disable nó, và hiển thị một dòng ngắn giải thích đang làm gì.

Các mẫu hiệu quả trên mạng không ổn định:

  • Vô hiệu hoá hành động chính sau khi chạm và giữ khoá tới khi có kết quả cuối cùng.
  • Hiển thị trạng thái “Pending” rõ ràng với chi tiết (số tiền, người nhận, số lượng).
  • Thêm view “Hoạt động gần đây” để người dùng xác nhận những gì đã gửi.
  • Nếu app vào nền, giữ trạng thái pending khi họ quay lại.
  • Ưu tiên một nút chính rõ ràng thay vì nhiều vùng chạm trên cùng màn hình.

Đôi khi yêu cầu thành công nhưng phản hồi bị mất. Hãy coi đây là kết quả bình thường, không phải lỗi khuyến khích bấm lại. Thay vì “Failed, try again”, hiển thị “Chúng tôi chưa chắc chắn” và đề xuất bước an toàn như “Kiểm tra trạng thái”. Nếu không thể kiểm tra trạng thái, giữ bản ghi pending cục bộ và báo sẽ cập nhật khi có kết nối.

Làm cho “Try again” rõ ràng và an toàn. Chỉ hiển thị khi bạn có thể lặp lại yêu cầu bằng cùng client-side request ID hoặc idempotency key.

Ví dụ thực tế: gửi checkout trong mạng không ổn định

Deploy with confidence
Deploy to AppMaster Cloud or your own cloud, with a setup that matches real mobile conditions.
Start now

Khách hàng trên tàu, tín hiệu chập chờn. Họ thêm hàng vào giỏ và chạm Pay. App phải kiên nhẫn, nhưng không được tạo hai đơn.

Một chuỗi an toàn:

  1. App tạo attempt ID client và gửi checkout kèm idempotency key (ví dụ UUID lưu với giỏ hàng).
  2. Yêu cầu chờ connect timeout rõ ràng, rồi read timeout lâu hơn. Tàu vào hầm, call timeout.
  3. App retry một lần, nhưng chỉ sau một khoảng delay ngắn và chỉ nếu nó chưa nhận phản hồi server.
  4. Server nhận yêu cầu thứ hai và thấy cùng idempotency key, nên trả lại kết quả ban đầu thay vì tạo đơn mới.
  5. App hiển thị màn hình xác nhận cuối cùng khi nhận success, dù nó tới từ lần retry.

Cache tuân theo quy tắc chặt chẽ. Danh sách sản phẩm, tuỳ chọn giao hàng và bảng thuế có thể cache ngắn (GET). Gửi checkout (POST) không bao giờ được cache. Ngay cả khi dùng HTTP cache, coi nó là trợ giúp đọc, không phải thứ “nhớ” thay bạn cho thanh toán.

Ngăn trùng lặp là kết hợp mạng và UI. Khi người dùng chạm Pay, nút bị vô hiệu hoá và màn hình hiển thị “Submitting order...” với một Cancel duy nhất. Nếu mất mạng, nó chuyển sang “Vẫn đang cố” và giữ cùng attempt ID. Nếu người dùng tắt app và mở lại, app có thể tiếp tục bằng cách kiểm tra trạng thái đơn bằng ID đó, thay vì yêu cầu họ trả tiền lại.

Checklist nhanh và bước tiếp theo

Nếu app của bạn “ổn trên Wi‑Fi văn phòng” nhưng vỡ trên tàu, thang máy hoặc vùng nông thôn, hãy coi đây là rào phóng phát hành. Công việc này ít liên quan đến mã lắt léo hơn là quy tắc rõ ràng có thể lặp lại.

Checklist trước khi phát hành:

  • Đặt timeout theo loại endpoint (login, feed, upload, checkout) và test trên mạng bị giới hạn băng thông và độ trễ cao.
  • Chỉ retry nơi thực sự an toàn, và giới hạn bằng backoff (một vài lần cho đọc, thường không cho ghi).
  • Thêm idempotency key cho mọi ghi quan trọng (thanh toán, đơn hàng, gửi form) để retry hoặc double tap không tạo bản sao.
  • Làm rõ quy tắc cache: cái nào có thể hơi cũ, cái nào phải tươi, và cái nào không bao giờ cache.
  • Hiển thị trạng thái rõ ràng: pending, failed, completed phải khác nhau, và app nên nhớ hành động đã hoàn tất sau khi khởi động lại.

Nếu một trong các mục là “quyết sau”, bạn sẽ có hành vi ngẫu nhiên khắp màn hình.

Bước tiếp theo để giữ quy tắc lâu dài

Viết một chính sách mạng một trang: phân loại endpoint, mục tiêu timeout, quy tắc retry và kỳ vọng cache. Áp dụng nó ở một chỗ (interceptor, factory client chia sẻ, hoặc wrapper nhỏ) để mọi thành viên đội có cùng hành vi mặc định.

Rồi làm một bài luyện chống trùng lặp ngắn. Chọn một hành động quan trọng (ví dụ checkout), mô phỏng spinner đông cứng, tắt app ép buộc, bật/tắt airplane mode, và bấm lại. Nếu bạn không chứng minh được an toàn, người dùng cuối sẽ tìm cách phá nó.

Nếu bạn muốn triển khai cùng quy tắc cho backend và client mà không phải wiring tay mọi thứ, AppMaster (appmaster.io) có thể giúp sinh backend production-ready và mã native mobile. Dù dùng công cụ gì, then chốt vẫn là chính sách: định nghĩa idempotency, retry, cache, và trạng thái UI một lần, rồi áp dụng nhất quán khắp luồng.

Câu hỏi thường gặp

What’s the first thing I should do before tweaking timeouts and retries?

Bắt đầu bằng cách xác định rõ “đúng” nghĩa là gì cho từng màn hình và hành động, đặc biệt là những việc phải chỉ xảy ra nhiều nhất một lần như thanh toán hoặc đặt hàng. Khi quy tắc rõ ràng, hãy thiết lập thời gian chờ, chính sách thử lại, cache và trạng thái UI phù hợp thay vì dựa vào mặc định của thư viện.

What are the most common symptoms users notice on slow or flaky networks?

Người dùng thường thấy vòng xoay vô tận, lỗi sau khi chờ lâu, hành động hoạt động lần hai hoặc kết quả trùng lặp như hai đơn hàng hay trừ tiền hai lần. Thường nguyên nhân là quy tắc thử lại và phân biệt “đang chờ” vs “thất bại” không rõ ràng, chứ không chỉ do tín hiệu yếu.

How should I think about connect, read, and write timeouts on mobile?

Dùng connect timeout cho thời gian chờ kết nối, write timeout cho việc gửi thân yêu cầu (upload), và read timeout cho việc chờ phản hồi sau khi gửi. Một mặc định hợp lý là timeout ngắn hơn cho các đọc rủi ro thấp và timeout dài hơn cho các gửi/submit quan trọng, kèm giới hạn UI rõ ràng để người dùng không chờ vô hạn.

If I can only set one timeout in OkHttp, which one should it be?

Đúng: nếu bạn chỉ đặt được một timeout, dùng callTimeout để giới hạn toàn bộ tác vụ từ đầu đến cuối, tránh chờ “vô hạn”. Sau đó có thể thêm connect/read/write để kiểm soát tốt hơn, đặc biệt cho upload và các phản hồi lớn.

Which errors are usually safe to retry, and which aren’t?

Bắt đầu chỉ thử lại những lỗi tạm thời như mất kết nối, DNS, hoặc timeout; thỉnh thoảng retry 502/503/504. Tránh retry 4xx và không tự động thử lại các thao tác ghi trừ khi bạn có bảo vệ idempotency, vì retry có thể tạo ra bản ghi trùng lặp.

How do I add retries without making the app feel stuck?

Dùng số lần thử nhỏ (thường 1–3) với backoff mũ và thêm một chút jitter để nhiều thiết bị không retry cùng lúc. Đồng thời giới hạn tổng thời gian thử lại để người dùng nhận được kết quả rõ ràng thay vì chờ spinner hàng phút.

What is idempotency, and why does it matter for payments and orders?

Idempotency nghĩa là lặp lại cùng một yêu cầu sẽ không tạo kết quả thứ hai, nên double tap hoặc retry không làm trừ tiền hay đặt chỗ hai lần. Với các thao tác quan trọng, gửi một idempotency key cho mỗi lần cố thử và tái sử dụng nó khi retry để server trả lại kết quả ban đầu thay vì tạo cái mới.

How should I generate and store an idempotency key on Android?

Tạo một khóa duy nhất khi người dùng bắt đầu hành động, lưu nó cục bộ với một bản ghi “pending”, và gửi kèm trong yêu cầu. Nếu retry hoặc app khởi động lại, tái sử dụng cùng khóa hoặc kiểm tra trạng thái bằng khóa đó, để không biến một ý định thành hai thao tác trên server.

What caching rules are safest for mobile apps on unreliable connections?

Chỉ cache dữ liệu có thể hơi cũ mà người dùng chịu được, và bắt buộc kiểm tra tươi cho những thứ liên quan đến tiền, bảo mật hoặc quyết định cuối cùng. Với các GET, ưu tiên độ tươi ngắn kèm revalidation và dùng ETag; với các write (POST/PUT/PATCH/DELETE) thì không cache; dùng no-store cho phản hồi nhạy cảm.

What UI patterns reduce double taps and accidental resubmits on slow networks?

Vô hiệu hoá nút chính sau lần chạm đầu tiên, hiện ngay trạng thái “Submitting…” và giữ trạng thái pending hiển thị kể cả khi app về background hoặc khởi động lại. Nếu phản hồi có thể bị mất, đừng khuyến khích người dùng chạm lại; thay vào đó hiển thị “Chúng tôi chưa chắc chắn” và đề xuất bước an toàn như kiểm tra trạng thái.

Dễ dàng bắt đầu
Tạo thứ gì đó tuyệt vời

Thử nghiệm với AppMaster với gói miễn phí.
Khi bạn sẵn sàng, bạn có thể chọn đăng ký phù hợp.

Bắt đầu