iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
Software Development

從 Fragment 到 Compose — 老 Android App 重寫的架構取捨系列 第 22 篇

Day 22|上傳策略:重試、退避與進度

  • 分享至 

  • xImage
  •  

動筆前先答(兩題)

1.【決策邊界|網路與資料】 App 要把三種檔案送出去:

  • 照片:一次 1~5 張,每張壓縮後約 300 KB。
  • 錄影:最長三分鐘,檔案可能上百 MB。
  • 診斷紀錄檔:配送員回報問題時才送,幾十 KB 的文字。

有三種做法:

  • (A) 用 multipart 上傳到既有 REST 後端,由後端存進物件儲存。
  • (B) App 內建物件儲存的存取金鑰,用 SDK 直接上傳。
  • (C) 先向後端要一個短效的預簽網址(presigned URL),App 再把檔案直接 PUT 到物件儲存。

請寫下:

  • 三種檔案各自預設選哪一種?在什麼條件下會改選另一種?至少想想這幾點:
    • 後端的請求大小上限與逾時設定。
    • 錄影傳到一半斷線,要從頭再傳,還是接著傳。
    • 檔案在物件儲存裡的名稱由誰決定;後端怎麼確認檔案真的傳上去了。
    • 金鑰或網址外流時,影響範圍有多大、多久。
  • (B) 的金鑰可以限制成「只能寫入某個資料夾」。這樣夠不夠?說出你的理由。
  • 診斷紀錄檔可能含裝置資訊與操作紀錄。它該跟照片走同一條管道、走另一支後端 API,還是乾脆讓配送員用系統的分享功能自己傳?各自的代價是什麼?

我的回答:(待補)

2.【預測再驗證|持久化與離線】 照片上傳的三層都各自寫了重試:

// 第一層:OkHttp 攔截器,遇到 IOException 或 5xx 就再送一次,最多再送 3 次
class RetryInterceptor(private val maxRetries: Int = 3) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var attempt = 0
        while (true) {
            try {
                val response = chain.proceed(chain.request())
                if (response.code < 500 || attempt == maxRetries) return response
                response.close()
            } catch (e: IOException) {
                if (attempt == maxRetries) throw e
            }
            attempt++
        }
    }
}

// 第二層:OkHttpClient 本身,retryOnConnectionFailure 維持預設值
val uploadClient = OkHttpClient.Builder()
    .addInterceptor(RetryInterceptor())
    .build()

// 第三層:WorkManager
@HiltWorker
class PhotoUploadWorker @AssistedInject constructor(
    @Assisted context: Context,
    @Assisted params: WorkerParameters,
    private val uploader: PhotoUploader,   // 用 uploadClient 把照片 PUT 到預簽網址;網址在排入佇列時取得,存在 Room
) : CoroutineWorker(context, params) {
    override suspend fun doWork(): Result {
        if (runAttemptCount >= 5) return Result.failure()
        val photoId = inputData.getString("photoId") ?: return Result.failure()
        return when (uploader.put(photoId)) {
            PutOutcome.Ok -> Result.success()
            PutOutcome.Retryable -> Result.retry()   // IOException 或 5xx
            PutOutcome.Rejected -> Result.failure()  // 其他 4xx
        }
    }
}
// 排入佇列時:網路連上才執行、EXPONENTIAL 退避、起始 30 秒

下面每個情境,先寫下你的預測,再用 MockWebServer 加 WorkManager 的測試工具(work-testing)或實機驗證:

  1. 伺服器每次都回 503。伺服器總共收到幾次請求?從第一次執行到 Worker 回傳 failure,大約經過多久?
  2. 伺服器讀完整個請求內容後直接斷線,不回應(MockWebServer 有「收完請求就斷線」的設定)。伺服器總共收到幾份完整的照片?OkHttp 自己的 retryOnConnectionFailure 有沒有參與?
  3. 預簽網址 15 分鐘後過期,過期後物件儲存回 403。手機一開始就收不到訊號,之後每次執行都失敗。第幾次執行開始拿到 403?拿到 403 之後,這張照片的下場是什麼?
  4. 上傳到 80% 時斷線。下一次執行從幾 % 開始傳?畫面用 getWorkInfoByIdFlow 讀 Worker 以 setProgress 回報的進度,在兩次執行之間,進度條會顯示什麼?

最後回答:

  • 重試應該放在哪一層:攔截器、OkHttp,還是 Worker?每一層各自負責哪一種失敗?
  • 預簽網址應該在什麼時候取得:排入佇列時,還是每次執行時?

我的回答:(待補)


上一篇
Day 21|離線佇列:Room + WorkManager
系列文
從 Fragment 到 Compose — 老 Android App 重寫的架構取捨 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言