Weather Pal 開發經驗揭示 Android 天氣應用程式的可靠性設計教訓
What building an Android weather app taught me about designing for reliability
作者以自建天氣應用程式 Weather Pal 的經驗,指出 Android 可靠性問題不只在伺服器故障,也包括背景更新延誤和未經授權請求耗盡 API 配額。他改為在資料顯示時檢查快取資料年齡,並以資料新鮮度控制裝置端 AI 洞見生成;網關則按 Play Integrity API 的驗證結果調整速率限制,而非直接封鎖驗證失敗的用戶。
I spent 13 years building Android apps, and for most of them, the backend was someone else’s problem. Building Weather Pal, my own forecast app, broke that habit fast. It taught me that mobile reliability is rarely about whether your server responded. It is about what happens to your data between fetch and display, and about who else is drinking from the pipe you pay for. Two incidents made that concrete. In the first, the operating system quietly stopped refreshing my data. In the second, scripts I had never authorised drained the quota that would have refreshed it. Neither showed up as an error.
Weather Pal is a Kotlin and Jetpack Compose app — hourly and daily forecasts, AQI, radar, weather alerts and a layer of plain-language insights generated on device with TensorFlow Lite. Forecasts come from Open-Meteo and MET Norway, official US alerts from the National Weather Service API and every request is relayed through a gateway I run rather than called straight from the handset. Coordinates are rounded to roughly city-block precision before they leave; there is no account and favorites and search history never reach a server, which is how my privacy policy puts it. I am deliberate about that wording, because you cannot get a forecast for a place without telling someone the place. The questions are how much, and whether you say so plainly.
The outage that existed only on the home screen
The widget was the feature I was proudest of and the first to betray me. It refreshed through a periodic WorkManager job, the textbook-correct way to do background work on modern Android. Then a beta tester sent me a screenshot: His widget read 31°C and clear skies, and his photo out the same window, taken seconds later, showed a downpour. The AQI figure was roughly half the live reading. Nothing had failed. Crashlytics was clean; the gateway had returned 200s all day. His device had simply placed my app in a restrictive App Standby bucket, and Doze mode had deferred my supposedly periodic worker for something like fourteen hours. The widget was faithfully rendering the last thing it had fetched, with no idea it was from the previous evening.
Android was not wrong. Deferring background work to protect battery is correct behavior, and a weather app that drains a battery earns the one-star review it gets. The bug was mine: I had read “periodic” as a promise rather than a hint, and had been treating the operating system as an environment my code runs in rather than a dependency with its own priorities and no obligation to tell me when it overrules me.
So, I stopped asking whether a fetch had succeeded and started asking, at render time, how old the cached data actually was. Every payload now carries its upstream observation timestamp, and every surface that draws it checks that age first. I also put the age on screen, the change I still have to defend in conversations. “Updated 6 min ago” under the conditions; a plain admission when data crosses the threshold, instead of a cheerful number rendered with total confidence.
The on-device AI layer pushed this harder than I expected. Weather Pal generates a short natural-language line from current conditions — something like “air quality is worsening through the evening, worth closing the windows before seven.” Fed stale inputs, the model produced the same fluent, confident sentence about conditions that had ended hours earlier. That is the uncomfortable property of generated text: its confidence does not degrade when its inputs do. A wrong number at least looks like a number. A wrong sentence looks like advice. Insight generation is now gated on data age, and the card explains its silence rather than filling the space. It was also a measurement error of the kind Google’s SRE book describes in its chapter on embracing risk — I was tracking request success rate, and nobody installs a weather app to experience a network layer. The indicator that mattered was data age at the moment of display.
The bots were making my data stale
The second incident arrived as a billing alert. The commercial provider I keep as a fallback started returning 429s partway through the month, which is not an outage — it is the bill telling you that you have spent what you agreed to spend. Real users fell back to cache, the cache went stale and my freshness problem had a new cause: somebody else was spending the requests.
My gateway endpoint was unauthenticated. It took a latitude and longitude and returned clean, normalized, unit-converted forecast data — precisely what you would want if you did not feel like paying for a weather API. A handful of addresses were pulling many times the volume of my entire user base, on a tidy schedule, with no app signature attached. I had not built a weather app with a gateway. I had built a free weather API with an app attached to it.
The answer was attestation — proving that a request came from a genuine, unmodified build of my app on a real device rather than from a script. On Android, that is the Play Integrity API, which returns a signed verdict about the app binary and the device that my gateway checks before spending an upstream request. For the lookup exposed on the website, I used Cloudflare Turnstile — a browser challenge belongs on a browser surface, and dropping a CAPTCHA into a native app is a confession that you have run out of ideas. The iOS counterpart, App Attest, is worth planning for early: each installation generates a hardware-backed key and signs its assertions, and those keys are per-install and do not survive a restore from backup, so a legitimate user replacing a lost phone looks exactly like an attacker.
One uncomfortable footnote: a meaningful share of the excess traffic was mine. My original retry logic was the one everybody writes first — catch, wait a second, try again, five times — invisible on a desk connection, constant on a phone moving through a metro tunnel, and arriving across a fleet in synchronized waves. Amazon’s Builders’ Library explains the mechanism better than I can in its piece on timeouts, retries and backoff with jitter. I had implemented neither the backoff nor the jitter, and I had hand-rolled a loop inside a worker that already had a perfectly good backoff policy built in. Before blaming scrapers for your quota, read your own client.
Then attestation broke things for real people
My first build treated the verdict as a gate: fail attestation, get nothing. Within a day I had locked out a user on a custom ROM, someone on an older device whose hardware-backed verdict was unavailable, my own QA emulators and briefly myself. Play Integrity also carries a network round trip and a request quota of its own, so I had introduced a fresh dependency into the cold-start path of an app whose entire value proposition is being fast at 7am.
I had traded a cost problem for an availability problem, a worse trade than it sounds: the cost problem showed up on a dashboard, the availability problem only to the people it silenced. Rooted phones and de-Googled builds are a choice plenty of privacy-minded users make deliberately — and privacy is a good part of why I keep coordinates coarse and run my own gateway, so blocking those users was incoherent as well as unkind.
What works now is treating attestation as a signal feeding a budget rather than a gate. A strong verdict earns a generous rate limit and live upstream calls. A weak or absent one is still served, from a coarser cached tier with a tighter limit and no premium fetches. Edge cases get a slightly worse app; scrapers get something too slow and stale to bother automating. Upstream spend dropped by most of the excess, and nobody had to email me about being locked out of the weather.
The habit I kept from all of this is unglamorous. Before writing error handling for any dependency — an API, the OS scheduler, an attestation service — I spend an hour writing down what a plausible wrong answer from it would look like. Not an outage: a well-formed, fast, confidently false answer. A stale timestamp. An AQI in a different index standard. A failed verdict from a real person. That hour has surfaced more defects than every retry I have written since, because error handling only covers the failure modes you were imaginative enough to picture. And I am warier now of metrics that only flatter me. My crash-free rate was excellent and irrelevant to both incidents. If a dashboard has never told you something you did not want to hear, be curious about what it is not measuring.
來源:InfoWorld · infoworld.com