Skillversitas

Networking in Kivy

A Kivy app that talks to the network - fetching a feed, posting a form, polling a status - has one hard constraint that shapes everything: the UI thread. A blocking network call on the UI thread freezes the render and event loops, so the app hangs until the request returns. The pattern that solves it is the same one for any heavy work: do the request off the UI thread (a Thread), and bring the result back to the UI thread with the Clock before touching any widget. This chapter covers that pattern with requests, the request lifecycle, error handling, and the cancellation that a polling app needs.

The Pattern: Thread Out, Clock Back

import threading
import requests
from kivy.clock import Clock

class FeedScreen:
    def __init__(self):
        self.label = None          # bound in the real app
        self._session = requests.Session()

    def load_feed(self):
        # 1. show the loading state on the UI thread
        self.label.text = "Loading..."
        # 2. do the request OFF the UI thread
        threading.Thread(target=self._fetch, daemon=True).start()

    def _fetch(self):
        # this runs in the worker thread - NO widget access here
        try:
            resp = self._session.get("https://api.example.com/feed",
                                     timeout=5)
            resp.raise_for_status()
            items = resp.json()
        except Exception as e:
            # bring the ERROR back to the UI thread too
            Clock.schedule_once(lambda dt: self._on_error(str(e)), 0)
            return
        # 3. bring the RESULT back to the UI thread
        Clock.schedule_once(lambda dt: self._on_loaded(items), 0)

    def _on_loaded(self, items):
        # back on the UI thread: safe to touch widgets
        self.label.text = f"{len(items)} items"

    def _on_error(self, msg):
        self.label.text = f"Error: {msg}"

The three-step shape is the entire discipline. load_feed sets the loading state on the UI thread and starts a daemon Thread for the request. The _fetch worker does the HTTP and touches no widgets - it catches the exception and hands both the success and the failure to the UI thread via Clock.schedule_once(..., 0), which schedules the callback on the next iteration of the event loop. The _on_loaded/_on_error callbacks run on the UI thread, where widget access is safe. The timeout on the request is essential - a hung connection with no timeout freezes the worker forever and the loading state never clears. This is the standard Kivy networking shape, and it is the same shape for any off-thread work (the threading chapter is the general version).

The Request Lifecycle and State

class ApiClient:
    """A thin wrapper that owns the session and the request state."""
    def __init__(self, base):
        self.base = base
        self.session = requests.Session()
        self.state = "idle"        # idle | loading | ready | error

    def get(self, path, on_done, on_error):
        self.state = "loading"
        threading.Thread(target=self._get, args=(path, on_done, on_error),
                         daemon=True).start()

    def _get(self, path, on_done, on_error):
        try:
            r = self.session.get(self.base + path, timeout=5)
            r.raise_for_status()
            data = r.json()
            Clock.schedule_once(lambda dt: self._set("ready",
                                                      lambda: on_done(data)), 0)
        except Exception as e:
            Clock.schedule_once(lambda dt: self._set("error",
                                                      lambda: on_error(e)), 0)

    def _set(self, state, fn):
        self.state = state
        fn()

For an app with several endpoints, wrap the session and the state in a small client. The state field (idle/loading/ready/error) is what the UI binds to - the loading spinner shows when state == "loading", the content when ready, the error text when error - so the screen is a view over the client's state, the same data-driven rule as the list. The client owns the Session (connection reuse across requests), the timeout, and the thread/clock handoff, so the screens call client.get(path, on_done, on_error) and never see the threading. Keeping the HTTP and the state in one place is what keeps ten networked screens from each reimplementing the thread/clock dance.

POST and the Form

def submit_recipe(self, name, notes, servings):
    payload = {"name": name, "notes": notes, "servings": servings}
    threading.Thread(target=self._post, args=(payload,), daemon=True).start()

def _post(self, payload):
    try:
        r = self.session.post(self.base + "/recipes", json=payload, timeout=5)
        r.raise_for_status()
        created = r.json()
        Clock.schedule_once(lambda dt: self._on_created(created), 0)
    except Exception as e:
        Clock.schedule_once(lambda dt: self._on_error(str(e)), 0)

def _on_created(self, created):
    # back on the UI thread: update the list, show a confirmation
    self.store.add(created)
    self.list.refresh()
    self.status.text = "Saved"

The POST is the same pattern with a body: the form's values are gathered on the UI thread, the payload is posted in the worker, and the result (the created record, the server's error) is brought back on the Clock. The on_created callback is where the UI reconciles - the store gets the new record, the list refreshes, the status label confirms - all on the UI thread. The form's submit button should disable itself while state == "loading" (the client's state again), which prevents the double-submit that a fast user's second tap would otherwise cause. The discipline is identical to the GET: no widgets in the worker, the result on the Clock, the timeout on the request.

Polling and Cancellation

class Poller:
    def __init__(self, url, interval, on_tick):
        self.url = url
        self.interval = interval
        self.on_tick = on_tick
        self._stop = threading.Event()
        self._thread = None

    def start(self):
        self._stop.clear()
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()

    def stop(self):
        self._stop.set()

    def _run(self):
        session = requests.Session()
        while not self._stop.is_set():
            try:
                r = session.get(self.url, timeout=5)
                r.raise_for_status()
                data = r.json()
                Clock.schedule_once(lambda dt, d=data: self.on_tick(d), 0)
            except Exception:
                pass            # a failed tick is skipped, not fatal
            self._stop.wait(self.interval)   # interruptible sleep

A polling app (a status dashboard, a live counter) repeats the fetch on an interval, and the two requirements are a clean start/stop and an interruptible wait. The threading.Event is the stop signal: stop() sets it, and the loop's self._stop.wait(interval) is the sleep that returns early when the event is set, so stopping the poller is immediate rather than waiting out the interval. The loop swallows a failed tick (one bad response is skipped, not fatal) and brings each successful result to the UI thread via the Clock. Stopping the poller when the screen is hidden (the app's on_pause, the screens chapter) and starting it on show is the lifecycle wiring that keeps a hidden screen from polling in the background - the battery and the API quota both thank you.

The Working Rules

  • Never block the UI thread. Every request runs in a worker thread; the UI thread only sets state and renders the result.
  • Clock back, always. The result and the error both return via Clock.schedule_once; a worker that touches a widget directly is a race waiting to happen.
  • Timeout on every request. A hung connection with no timeout is a permanent loading state; 5s is a sane default for an interactive app.
  • Own the session and the state. A small client wraps the Session, the state field, and the handoff, so the screens stay simple and the state is bindable.
  • Gate the UI on state. Disable the submit while loading, stop the poller when hidden - the state field drives the affordances, preventing double-submits and background polling.

Summary

Network in Kivy with the thread-out/clock-back pattern: set the loading state on the UI thread, run the request in a daemon Thread that touches no widgets, and bring both the result and the error back with Clock.schedule_once(..., 0) before touching anything. Put a timeout on every request, own the Session and a state field (idle/loading/ready/error) in a small client so the screens are views over the state, and gate the UI on it - disable the submit while loading, stop the poller when hidden. The POST form and the polling loop are the same shape with a body and a repeat, and the interruptible threading.Event wait makes the poller stop cleanly. The one rule underneath all of it: the UI thread renders, the worker thread fetches, and the Clock is the only door between them.