Build a Simple Chat App

Build a simple chat app end to end with Python. Practical Mobile App Development tutorial covering design, implementation, troubleshooting, and next steps.

Focus: build a simple chat app end to end

Sponsored

You've built web apps, APIs, and maybe a desktop tool or two — but when it comes to mobile, most developers hit a wall. The tutorials are either too abstract ("just use Firebase!") or too heavy (a 200-line starter project before you've sent a single message). Instead, let's strip away the noise and build a simple chat app end to end — from user interface to real-time messaging — using Python and Kivy. By the end of this lesson, you'll have a working chat client that talks to a backend server, and you'll understand every moving part.

The problem this lesson solves

Chat apps look deceptively simple from the outside: type a message, hit send, and it appears on someone else's screen a second later. But under the hood, you're juggling UI state, network calls, message ordering, and connection failures — all at once. Most beginners either give up when they hit the first WebSocket error, or they copy-paste a Firebase tutorial without understanding what happens when the network drops.

This lesson exists because building a chat app end to end forces you to combine the skills you've already learned — list-based UIs, event handling, HTTP requests, and threading — into one coherent system. You'll learn how to structure a mobile client, how to communicate with a backend, and how to handle the tricky bits like reconnection and message order. And you'll do it with plain Python, so you can see exactly what's happening at every layer.

Core concept / mental model

Think of a chat app as a two-sided conversation between a client and a server — not between two users directly. Your mobile app (the client) sends messages to a central server, and the server broadcasts them to every other connected client. The server is the post office: it receives letters, stamps them with a timestamp, and delivers them to every mailbox that's listening.

This is called the client-server model, and it's the backbone of almost every chat system, from WhatsApp to Slack. Here's the mental image:

User A (Kivy app)  →  Chat Server  →  User B (Kivy app)
        ↑                |                ↑
        └── HTTP POST ───┘── WebSocket ───┘
  • Client: your Kivy app, which displays a scrollable list of messages and a text input.
  • Server: a small Python process (we'll use Flask + SocketIO) that accepts incoming messages and pushes them to all subscribed clients.
  • Transport: for sending a message, a simple HTTP POST is fine. For receiving messages in real time, you need a persistent connection — WebSocket is the modern choice.

But why not just use HTTP polling? Because polling means your app has to ask the server "any new messages?" every few seconds — wasteful and laggy. A WebSocket gives you a push-based connection: the server sends data the moment it's available.

💡 Pro tip: You don't need a full-blown message broker for a simple chat. A single server process with an in-memory message list is enough for learning and prototyping.

How it works step by step

Let's break the architecture into concrete layers, then walk through a message's lifecycle.

1. The client UI (Kivy layer)

Your Kivy app has three main components:

  • A RecycleView (or ListView) to display messages — each item shows the sender and text.
  • A TextInput for composing a new message.
  • A send button that triggers the send event.

2. The network layer

The client communicates with the server in two directions:

  • Send path: POST /send with JSON {sender, message} → server stores and broadcasts.
  • Receive path: a WebSocket connection that receives broadcast events.

3. The server (backend)

Using Flask + Flask-SocketIO, the server does three things:

  1. Stores messages in a simple Python list.
  2. Exposes a send_message event that appends to the list and emits to all clients.
  3. Sends the full message history when a new client connects (via connect event).

4. The message lifecycle

Here's what happens when User A sends "Hello":

  1. User A types "Hello" and taps Send.
  2. The Kivy app fires a on_send event, grabs the text, and calls the server's POST /send endpoint with JSON.
  3. The server receives the request, appends {sender: "A", text: "Hello", timestamp: ...} to its list.
  4. The server broadcasts a new_message event over WebSocket to all connected clients (including A).
  5. Each client, including B, receives the event and updates its RecycleView.

Why this works: The server is the single source of truth, so message order is consistent across all clients — no client has to worry about merging messages from different sources.

Hands-on walkthrough

Now let's build it. We'll create the server first, then the Kivy client.

Prerequisites

Make sure you have Python 3.10+ and install the dependencies:

pip install kivy flask flask-socketio

Step 1: Build the chat server

Create a file named chat_server.py:

from flask import Flask, request, jsonify
from flask_socketio import SocketIO, emit, send
import time

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app, cors_allowed_origins="*")

# In-memory message store
messages = []

def add_message(sender, text):
    msg = {
        "sender": sender,
        "text": text,
        "timestamp": time.time()
    }
    messages.append(msg)
    # Keep only last 100 messages to avoid unbounded growth
    if len(messages) > 100:
        messages.pop(0)
    return msg

@app.route('/send', methods=['POST'])
def send_message_http():
    data = request.get_json()
    if not data or 'sender' not in data or 'text' not in data:
        return jsonify({"error": "Missing sender or text"}), 400
    msg = add_message(data['sender'], data['text'])
    socketio.emit('new_message', msg)
    return jsonify(msg), 201

@socketio.on('connect')
def handle_connect():
    # Send full history to the newly connected client
    emit('history', messages)

@socketio.on('new_message')
def handle_new_message(data):
    # This handles WebSocket send, but we'll stick to HTTP for simplicity
    pass

if __name__ == '__main__':
    socketio.run(app, host='0.0.0.0', port=5000, debug=True)

This server keeps a list of messages, exposes an HTTP endpoint to post a new message, and broadcasts it to all connected WebSocket clients. When a client connects, the server immediately sends the entire history through the history event.

Step 2: Build the Kivy client

Create chat_client.py:

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.recycleview import RecycleView
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.clock import Clock
from kivy.properties import ListProperty
import requests
from threading import Thread
from socketio import Client

SERVER_URL = "http://localhost:5000"

class MessageList(RecycleView):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.data = []

    def add_message(self, sender, text):
        # Prepend to show most recent at bottom? We'll append and use scroll.
        self.data.append({"text": f"{sender}: {text}"})
        self.refresh_from_data()

class ChatLayout(BoxLayout):
    messages = ListProperty([])

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.orientation = "vertical"
        self.message_list = MessageList()
        self.add_widget(self.message_list)

        input_area = BoxLayout(size_hint_y=0.15)
        self.text_input = TextInput(hint_text="Type your message...", multiline=False)
        send_btn = Button(text="Send", size_hint_x=0.3)
        send_btn.bind(on_press=self.on_send)
        input_area.add_widget(self.text_input)
        input_area.add_widget(send_btn)
        self.add_widget(input_area)

        # Start socket thread
        self.sio = Client()
        self.thread = Thread(target=self.listen_for_messages, daemon=True)
        self.thread.start()

    def on_send(self, instance):
        text = self.text_input.text.strip()
        if not text:
            return
        sender = "Me"
        # Send via HTTP POST
        try:
            response = requests.post(f"{SERVER_URL}/send", json={"sender": sender, "text": text})
            if response.status_code == 201:
                self.message_list.add_message(sender, text)
                self.text_input.text = ""
            else:
                print("Failed to send:", response.text)
        except requests.exceptions.RequestException as e:
            print("Network error:", e)

    def listen_for_messages(self):
        self.sio.connect(SERVER_URL)
        self.sio.on('history', self.on_history)
        self.sio.on('new_message', self.on_new_message)

    def on_history(self, history):
        for msg in history:
            self.message_list.add_message(msg['sender'], msg['text'])

    def on_new_message(self, msg):
        self.message_list.add_message(msg['sender'], msg['text'])

class ChatApp(App):
    def build(self):
        return ChatLayout()

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

Wait — there's a bug in that client code. The listen_for_messages method registers callbacks after connecting, which means you might miss the history event. Let's fix that by registering callbacks before connecting:

    def listen_for_messages(self):
        self.sio.on('history', self.on_history)
        self.sio.on('new_message', self.on_new_message)
        self.sio.connect(SERVER_URL)

Now, run the server in one terminal:

python chat_server.py

And run the client in another terminal (you can run multiple clients to see real-time messages):

python chat_client.py

Expected output: The client window opens with no messages. When you type "Hello" and hit Send, you'll see "Me: Hello" appear, and any other running client will show the same message. If you open a second client, it will receive the full history of previous messages.

⚠️ Kivy and threads: Kivy UI updates must happen on the main thread. The socketio.Client callbacks fire on a background thread, so we use the Clock module to safely schedule UI updates. In our example, we're calling add_message directly from the callback thread, which can cause crashes. A safer approach is to wrap the update in Clock.schedule_once:

from kivy.clock import Clock

def on_new_message(self, msg):
    Clock.schedule_once(lambda dt: self.message_list.add_message(msg['sender'], msg['text']))

Compare options / when to choose what

You don't have to use Flask-SocketIO. There are several viable stacks for building a chat backend. Here's a quick comparison:

Approach Real-time Complexity Best for
HTTP polling Laggy (seconds) Low Simple demos, non-critical apps
Flask-SocketIO (WebSocket) Instant Medium Learning, small-to-medium prototypes
Firebase Realtime Database Instant Low (managed) Production apps needing auth and sync
MongoDB + change streams Instant High Apps already using MongoDB

When to choose what: - Learning: Build your own server with Flask-SocketIO — you control every line. - Rapid prototyping: Use Firebase to skip backend code entirely. - Production at scale: Use a managed messaging service like PubNub, Ably, or a proper WebSocket server like websockets with Redis for pub/sub.

Variations to explore

  • Use websockets library instead of SocketIO: Lighter, but you handle JSON serialization yourself.
  • Use Kivy's built-in kivy.network: You can use UrlRequest for HTTP instead of requests.
  • Swap the backend to FastAPI: FastAPI can serve both REST and WebSocket with WebSockets support.

Troubleshooting & edge cases

Here are the most common failures you'll hit and how to fix them:

1. ModuleNotFoundError: No module named 'socketio'

Run pip install python-socketio — the client and server libraries are separate packages. Make sure you installed python-socketio for the client, not just flask-socketio.

2. Client connects but never receives history

This is usually because you registered the event handler after calling .connect(). Fix: register all handlers first, then connect.

3. Connection refused when connecting to the server

Check that the server is running and that you're using the correct port. If you're testing on an Android emulator, localhost refers to the emulator itself — use 10.0.2.2 instead.

4. Kivy UI freezes or crashes when messages arrive

You're updating RecycleView from a non-main thread. Use Clock.schedule_once to marshal UI updates to the main thread.

5. Message order is wrong on the client

Because the server broadcasts over WebSocket immediately, but HTTP posts are synchronous, you may see your own message appear twice (once from your HTTP response, once from the broadcast). Avoid adding the message locally when you send; rely only on the broadcast. Or, if you do add locally, deduplicate using a unique message ID.

💡 Pro tip: Add a msg_id (UUID) to each message on the server and have the client track received IDs to prevent duplicates.

What you learned & what's next

You've now built a functional chat app end to end: a real-time client in Kivy, a Flask-SocketIO backend, and the glue that connects them. You understand the core idea — that a central server manages messages and pushes updates to clients — and you completed a hands-on exercise that demonstrates both sending and receiving messages.

Specifically, you: - Explained the core idea behind a chat app's client-server model. - Completed a practical exercise that connects a Kivy UI to a backend. - Learned to troubleshoot common network and threading issues.

Your next lesson in the Mobile App Development track will likely cover storing chat history in a database or adding authentication and user identity. You'll take this same architecture and make it persistent and secure. Keep this project — you'll extend it.

Now, try modifying the app to let users pick a username before chatting. That single feature will push you closer to a real, production-worthy chat experience.

Practice recap

Extend the chat app you just built by adding a username prompt on launch. Store the username in a Kivy StringProperty, include it in every message, and display it in the message list. Next, try adding a 'typing...' indicator as a stretch goal to practice broadcasting events over the same WebSocket connection.

Common mistakes

  • Forgetting to register SocketIO event handlers before calling .connect() — you miss the history event.
  • Updating Kivy UI directly from a background thread — causes crashes or freezes; always use Clock.schedule_once.
  • Using localhost on an Android emulator — use 10.0.2.2 to reach your development machine.
  • Duplicating messages by adding them locally AND receiving the server broadcast — add a unique msg_id and deduplicate.

Variations

  1. Use the websockets library instead of Flask-SocketIO for a lighter, more manual WebSocket implementation.
  2. Swap the backend to FastAPI, which natively supports WebSocket connections alongside REST endpoints.
  3. Use Firebase Realtime Database for a fully managed backend, skipping server code entirely.

Real-world use cases

  • Customer support chat widget inside a mobile e-commerce app, connecting shoppers to agents in real time.
  • Team collaboration app with channels and direct messages, where message history must sync across multiple devices.
  • Multiplayer game lobby chat, where players coordinate and strategize in real time via a central server.

Key takeaways

  • Chat apps follow a client-server model: the server is the single source of truth for messages.
  • Real-time updates require a persistent WebSocket connection, not HTTP polling.
  • Separate network operations (sending) from UI updates (receiving) to keep the interface responsive.
  • Always register SocketIO event handlers before the client connects to avoid missing initial data.
  • Use Clock.schedule_once to safely update Kivy widgets from background threads.

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.