Cache Network Data Offline

Cache network data for offline access in your mobile app. Learn how to store responses locally to ensure your app works without an internet connection, with a hands-on Kivy example, edge cases, and troubleshooting tips.

Focus: cache network data for offline access

Sponsored

Is your mobile app still useful when the Wi-Fi drops or the subway goes underground? If your answer is “not really,” then you're leaving users stuck staring at a spinner. In this lesson, you'll fix that by learning how to cache network data for offline access — so your app feels fast, resilient, and respectful of your users' data plans. We'll explore the core concepts, compare strategies, and build a hands-on Kivy example that keeps your app useful even with no signal.

The problem this lesson solves

Modern mobile apps are networked by nature — but networks fail, signal drops, and users travel through dead zones. Without a caching strategy, every launch forces a fresh fetch, which means:

  • Slow start times — users wait every time, even for the same content.
  • Broken UX offline — the app is completely useless without a connection.
  • Wasted bandwidth — re-downloading identical payloads drains data plans.
  • Poor server resilience — your API gets hammered with repeated identical requests.

Think of the last time you opened a news app on a plane. If it showed a blank screen, the developer skipped caching. If it showed yesterday's headlines with a "You're offline" banner, that's caching done right. This lesson closes that gap.

Core concept / mental model

Cache network data for offline access means storing a copy of a successful network response in local storage so that future reads can succeed without the network. Let's build a mental model.

Imagine a library you visit every day. Each trip costs time and energy. A smart move: take a photo of the books you usually read and keep it in your pocket. When the library is closed, you still have your photos. When it's open, you can update them. The library is your API, and the photos are your cache.

In code, a cache is defined by three operations:

  • Read: return the cached value if it exists and is fresh.
  • Write: store a new successful response with a timestamp.
  • Invalidate: delete or refresh entries when they become stale.

A robust caching layer is like a service layer between your UI and your network client. It intercepts every request and decides whether to go to the network or answer from disk. That decision is guided by a cache policy — usually one of:

  • Cache-first: serve from cache, then update in the background.
  • Network-first: try the network, then fall back to cache on error.
  • Stale-while-revalidate: serve stale cache immediately, then fetch fresh data in the background.

Key definitions

  • Cache hit — the local copy satisfies the read.
  • Cache miss — no valid local copy; must fetch from network.
  • TTL (time-to-live) — how long an entry is considered fresh.
  • Stale — an entry older than its TTL, but still present.

How it works step by step

Here's a generic flow for an offline-first request handler. You can adapt this to any framework — Kivy, BeeWare, or a pure Python module used by your app.

  1. Intercept the request at your data layer, not inside every UI callback.
  2. Check the cache — look for a saved response for this exact resource key.
  3. Validate freshness — compare the stored timestamp against the TTL.
  4. If fresh → return the cached data. No network call.
  5. If stale or missing → try the network.
  6. On success — save the response with a new timestamp, then return it.
  7. On failure — if a stale copy exists, return that (possibly with a flag), otherwise raise a clear error.
  8. Handle writes and deletions the same way — update or remove the cached copy.

This logic is the same whether you cache JSON payloads, images, or entire API responses. The main difference is your storage backend: for simple data, a JSON file or Python shelve works; for binary assets, save bytes to a file.

Hands-on walkthrough

We'll build a small but practical example: a Kivy app that fetches a list of posts from JSONPlaceholder and caches them to disk. You'll see the full request-and-cache flow, and it'll work offline after the first successful fetch.

Setup

Install the requirements if you haven't already:

pip install kivy requests

Step 1: A cache store for responses

Write a simple JSON-file cache. It stores each response under a unique key derived from the URL.

import json
import os
import time
from pathlib import Path

class JsonCache:
    def __init__(self, directory="cache", ttl=300):
        self.directory = Path(directory)
        self.directory.mkdir(parents=True, exist_ok=True)
        self.ttl = ttl  # seconds an entry stays fresh

    def _path(self, key):
        return self.directory / f"{key}.json"

    def get(self, key):
        """Return (data, fresh) if cache exists, else (None, False)."""
        path = self._path(key)
        if not path.exists():
            return None, False
        with open(path, "r", encoding="utf-8") as f:
            entry = json.load(f)
        age = time.time() - entry["timestamp"]
        return entry["data"], age < self.ttl

    def set(self, key, data):
        entry = {"timestamp": time.time(), "data": data}
        with open(self._path(key), "w", encoding="utf-8") as f:
            json.dump(entry, f, indent=2)

Step 2: A fetch function with cache fallback

Now combine requests with the cache to handle online and offline cases explicitly.

import requests

def fetch_posts(cache, key="posts", force_refresh=False):
    """Return posts from cache, or fetch and cache on miss."""
    if not force_refresh:
        data, fresh = cache.get(key)
        if fresh:
            print("Serving from cache (fresh)")
            return data

    try:
        response = requests.get("https://jsonplaceholder.typicode.com/posts", timeout=5)
        response.raise_for_status()
        posts = response.json()
        cache.set(key, posts)
        print("Fetched and stored in cache")
        return posts
    except requests.RequestException as e:
        data, fresh = cache.get(key)
        if data:
            print(f"Network error, serving stale cache: {e}")
            return data, False
        print(f"Network error and no cache available: {e}")
        return None

Note: I returned (data, False) in one branch to signal staleness — you might prefer a tuple consistently. For simplicity, we'll use that pattern below.

Step 3: The Kivy UI

Let's wire it into a minimal Kivy screen with a refresh button to demonstrate the flow.

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label

class OfflineApp(App):
    def build(self):
        self.cache = JsonCache(ttl=60)  # 1 minute freshness

        layout = BoxLayout(orientation="vertical", padding=10)
        self.label = Label(text="Press refresh to load posts", size_hint_y=0.8)
        refresh_btn = Button(text="Refresh posts", size_hint_y=0.2)
        refresh_btn.bind(on_press=self.load_posts)

        layout.add_widget(self.label)
        layout.add_widget(refresh_btn)
        return layout

    def load_posts(self, *args):
        result = fetch_posts(self.cache, key="posts")
        if result is None:
            self.label.text = "No internet and no cache. Try again later."
        else:
            posts, fresh = result
            preview = "\n".join(f"{p['id']}: {p['title']}" for p in posts[:5])
            status = "fresh" if fresh else "stale"
            self.label.text = f"Showing {len(posts)} posts ({status})\n{preview}"

if __name__ == "__main__":
    OfflineApp().run()

Expected output (first run online): a list of post titles in the label. Run it again offline (disconnect Wi‑Fi) — you'll still see the same list, now served from cache. That's offline access in action.

Pro tip: In production, store a last_updated string next to the timestamp so you can display "Last updated at 14:32" — it builds trust with users.

Compare options / when to choose what

Not all caching is the same. Here's a quick comparison of common strategies for a mobile app.

Strategy Speed Offline support Data freshness Implementation effort Use case
Cache-first (stale-while-revalidate) Instant Perfect Stale until revalidate Moderate News feeds, dashboards
Network-first with fallback Slow on miss, then fast Good Always fresh Low User profile, checkout data
TTL-only cache Fast while fresh Limited to TTL Good until expiry Low Weather, stock prices
No caching Slow every time None Always fresh None Real-time chat (with socket fallback)

When to choose what:

  • Cache-first — for content that rarely changes and where perceived speed matters more than freshness. Great for list views.
  • Network-first — for critical operations where stale data is risky (e.g., PIN validation). But ensure you still cache the last success so the app isn't blank offline.
  • TTL-only — the simplest to implement; perfect for data with a known lifetime.

Variations in practice

  • SQLite as a cache store — use a database instead of JSON files to support large datasets and more complex queries. This is a natural next step as your data grows.
  • Thirty-party libraries — on Android you might use OkHttp's cache or Retrofit's Kaptcha; on iOS URLCache. In Python, packages like requests-cache can drop into your fetch code.
  • Image caching — for photos, caching files (PNG/JPEG) is different: save bytes to app_storage, store metadata in JSON, and send ETag headers to minimize transfers.

Troubleshooting & edge cases

Caching sounds simple, but several pitfalls can bite you:

  • Stale JSON structure — if you update your app and change the API response shape, old caches could break your parser. Always include a schema version in your cache entry.

python entry = {"version": 1, "timestamp": ..., "data": ...} if entry["version"] != 1: # treat as miss

  • Disk full or permission errors — writing cache can fail on low storage. Wrap cache.set in a try/except and treat failure as “no cache” rather than crashing.
  • Too many files — storing one file per request can bloat. Use a single JSON file with a dict, or use a proper database.
  • Time zone drift — use time.time() (UTC) for timestamps, never local time.
  • Offline flag not detectedrequests.exceptions.ConnectionError and timeouts both mean offline, but so do SSL errors. Always catch the broad requests.RequestException.
  • Cache poisoning — if a server returns a malformed response, your cache could store it. Validate the shape before saving (e.g., check that it's a list of dicts).

What you learned & what's next

You now understand the core idea behind cache network data for offline access — you can explain the difference between cache-first, network-first, and TTL-based strategies, and you've completed a practical exercise that turns a network-only Kivy app into one that survives offline conditions. You implemented a reusable JsonCache, combined it with requests, and saw the fallback logic in action.

Next in the track, you'll likely move on to local databases for structured storage — think SQLite. Caching gives you offline access, but a database gives you query power, relationships, and the ability to handle larger datasets gracefully. With those two skills combined, you'll build apps that feel native and fast in any connection state.

Practice now: modify the cache TTL to 86400 (a day) and run the app twice with your network turned off between runs. Notice how fast the second run is? Also try changing the URL parameter to see that different keys create separate cache files. That's your cache working exactly as designed.

Practice recap

Now try this: change the cache TTL in the JsonCache to 86400 (one day) and run the app twice with your network switched off between runs. Notice that the second run is instant because it's served from cache. Then modify the URL to ?userId=1 and see that a new key creates a separate cache file — your cache is already working exactly as designed. Next explore the official requests-cache library to see how easily it integrates with your current code.

Common mistakes

  • Storing HTTP responses in memory only — you lose the cache every time the app restarts. Write to disk or a database instead.
  • Not handling the case where the cache exists but is stale — you must either refresh or clearly label it as outdated, not silently show wrong data.
  • Using local clock timestamps without timezone awareness — store UTC time (time.time()) to avoid drift and confusion.
  • Forcing network requests even when you have a fresh cache because you didn't implement a TTL check — you're wasting bandwidth and battery.
  • Assuming a timeout error is the only offline signal — SSL errors, DNS failures, and connection resets all mean offline. Always catch the broad requests.RequestException.

Variations

  1. Use requests-cache library to add caching to any existing requests call with zero manual bookkeeping.
  2. Store cache in SQLite or a key‑value store like shelve instead of JSON files when you need to query larger datasets.
  3. Leverage HTTP ETag or If-Modified-Since headers to avoid downloading unchanged payloads even when online.

Real-world use cases

  • News apps that show yesterday's headlines while you're on a plane, letting you browse content without a connection.
  • Weather apps that save the last forecast so users can still see temperature and chances of rain in remote areas.
  • E‑commerce apps that cache product lists and cart info so shoppers can review items even during temporary loss of signal.

Key takeaways

  • Caching network data means storing a successful response locally to serve future reads without a network call.
  • A cache store needs three operations: read, write, and invalidate — plus a TTL to decide freshness.
  • Choose between cache-first, network-first, and TTL-only based on how critical freshness is for your data.
  • Your fetch logic must gracefully handle stale caches: serve them offline but flag them as outdated.
  • Always include a schema version in cached entries to avoid breaking on app updates.
  • Ideal next step after caching is adding a local database for structured querying and larger data volumes.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.