Creating a Fitness Tracker App with Python Reflex

By on 16 January 2025

In this post, I will build a simple fitness tracker app using Python Reflex.

Reflex is a Python library that allows you to create reactive applications using a functional and declarative approach.

We will use Reflex to create a simple fitness tracker app that allows you to log the amount of workouts completed per week.

This is how it will look:

Screenshot 2025 01 15 at 18.20.30

Install Reflex

After making a new directory and cd’ing into it, I’ll use uv (anybody not yet using this amazing tool? πŸ’‘) to initialize a project and install Reflex:

√ fitness_tracker  $ uv init --no-workspace --no-package
Initialized project `fitness-tracker`
√ fitness_tracker (main) $ rm hello.py
√ fitness_tracker (main) $ uv add reflex
Using CPython 3.13.0
Creating virtual environment at: .venv
Resolved 81 packages in 602ms
Prepared 4 packages in 1.47s
Installed 72 packages in 305ms

The awesome thing about uv is that you don’t have to worry about clearing and activating a virtual environment. It’s also super fast (listen to the creator talk about it on our podcast).

Initialize and configure Reflex

Next up I will run reflex init which presents me with the following menu. I am selecting the default blank option to start from scratch:

$ uv run reflex init
──────────────────────────────────────────────────────────────────────────────────────────────────── Initializing fitness_tracker ─────────────────────────────────────────────────────────────────────────────────────────────────────
[14:30:37] Initializing the web directory.                                                                                                                                                                               console.py:161

Get started with a template:
(0) blank (https://blank-template.reflex.run) - A blank Reflex app.
(1) ai - Generate a template using AI [Experimental]
(2) choose templates - Choose an existing template.
Which template would you like to use? (0):
[14:30:43] Initializing the app directory.                                                                                                                                                                               console.py:161
Success: Initialized fitness_tracker using the blank template
√ fitness_tracker (main) $ ls -C1
README.md
__pycache__
assets
fitness_tracker
hello.py
pyproject.toml
requirements.txt
rxconfig.py
uv.lock

Next we’ll configure the app by adding a database. To keep it simple I’ll just use a sqlite DB. Updating rxconfig.py:

config = rx.Config(
    app_name="fitness_tracker",
    db_url="sqlite:///reflex.db",
)

At this point we can run the dev server to verify the installation (note that the first time it will take a bit to do the initial compilation):

$ uv run reflex run
Info: The frontend will run on port 3001.
Info: The backend will run on port 8001.
Info: Overriding config value frontend_port with env var FRONTEND_PORT=3001
Info: Overriding config value backend_port with env var BACKEND_PORT=8001
───────────────────────────────────────────────────────────────────────────────────────────────────────── Starting Reflex App ─────────────────────────────────────────────────────────────────────────────────────────────────────────
[14:32:12] Compiling: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 13/13 0:00:00
───────────────────────────────────────────────────────────────────────────────────────────────────────────── App Running ─────────────────────────────────────────────────────────────────────────────────────────────────────────────
Info: Overriding config value frontend_port with env var FRONTEND_PORT=3001
Info: Overriding config value backend_port with env var BACKEND_PORT=8001
App running at: http://localhost:3001
Backend running at: http://0.0.0.0:8001

Skeleton app code

Reflex already created some boilerplate for us in fitness_tracker/fitness_tracker.py:

"""Welcome to Reflex! This file outlines the steps to create a basic app."""

import reflex as rx

from rxconfig import config


class State(rx.State):
    """The app state."""

    ...


def index() -> rx.Component:
    # Welcome Page (Index)
    return rx.container(
        rx.color_mode.button(position="top-right"),
        rx.vstack(
            rx.heading("Welcome to Reflex!", size="9"),
            rx.text(
                "Get started by editing ",
                rx.code(f"{config.app_name}/{config.app_name}.py"),
                size="5",
            ),
            rx.link(
                rx.button("Check out our docs!"),
                href="https://reflex.dev/docs/getting-started/introduction/",
                is_external=True,
            ),
            spacing="5",
            justify="center",
            min_height="85vh",
        ),
        rx.logo(),
    )


app = rx.App()
app.add_page(index)
  • A Reflex app consists of a State class and a index function that returns a Component.
  • You can see the State class as the back-end of the app, it holds the data and methods to manipulate it.
  • The index function is the front-end of the app, it returns a Component that defines the UI.
  • Similar to Flask/FastAPI, there is an App class we should instantiate. We bind the index page to the app using its add_page method.

Use a database to save workouts

Let’s start with defining a model to track our workouts. For this app I am only interested if the workout got done and when, not what I did per se.

Reflex uses sqlmodel as its ORM (object relational mapper), so we can add a single column table as easily as this:

from datetime import datetime
import reflex as rx
from sqlalchemy import Column, DateTime, func, and_
from sqlmodel import Field

class Workout(rx.Model, table=True):
    """Database model for a workout."""
    completed: datetime = Field(
        sa_column=Column(
            DateTime(timezone=True),
            server_default=func.now(),
        )
    )

In order to set the default value for theΒ completedΒ field, I needed theΒ server_defaultΒ argument of theΒ ColumnΒ constructor.

This will set the field to the current time when a new record is created (using completed: datetime = datetime.now() won’t work here, it would set a fixed datetime once 😱).

Sometimes you need to mix in SQLAlchemy features like this, as the ReflexΒ docs say:

SQLModel automatically maps basic python types to SQLAlchemy column types, but for more advanced use cases, it is possible to define the column type using sqlalchemy directly.

Reflex ships with Alembic so we can use makemigrationsmigrate to sync this model to the sqlite db we configured earlier, after doing an db init:

$ uv run reflex db init
$ uv run reflex db makemigrations --message "Add Workout model"
$ uv run reflex migrate

New to Alembic / db migrations? I made this beginner video about it.

Retrieving workouts

In the next couple of sections I will build out the back-end logic first by updating the State class. First we make a method to load workouts from the database:

from sqlalchemy import and_

WEEKLY_GOAL = 5

class State(rx.State):
    workouts: list[str] = []
    target: int = WEEKLY_GOAL
    current_week_offset: int = 0

    def load_workouts(self, week_offset: int = 0):
        today = datetime.now(timezone.utc)
        start_of_week = datetime(today.year, today.month, today.day) - timedelta(
            days=today.weekday()
        )
        start_of_week += timedelta(weeks=week_offset)
        end_of_week = start_of_week + timedelta(days=7)

        with rx.session() as session:
            db_workouts = (
                session.query(Workout)
                .filter(
                    and_(
                        Workout.completed >= start_of_week,
                        Workout.completed < end_of_week,
                    )
                )
                .all()
            )
            self.workouts = [
                workout.completed.strftime("%Y-%m-%d %H:%M") for workout in db_workouts
            ]
  • We set a weekly goal of 5 workouts (my current and pretty static goal).
  • We load workouts for the current week, if we want to see past or future weeks we can pass an offset into the method
  • We format the dates as strings for display, because State can only hold simple types, so no ORM objects. πŸ’‘
  • We use theΒ rx.session()Β context manager to get a session to query the db, very similar to sqlmodel and SQLAlchemy.

Properties (computed fields)

You can define properties in Reflex using theΒ @rx.varΒ decorator. I am defining a couple we’ll need in the front-end in a bit:

@rx.var
def progress(self) -> int:
    return len(self.workouts)

@rx.var
def progress_percentage(self) -> int:
    return int(self.progress / self.target * 100)

@rx.var
def goal_reached(self) -> bool:
    return self.progress >= self.target

@rx.var
def current_week(self) -> bool:
    return self.current_week_offset == 0

@rx.var
def yymm(self) -> str:
    dt = datetime.now(timezone.utc) + timedelta(weeks=self.current_week_offset)
    cal = dt.isocalendar()
    return f"{cal.year} - week {cal.week:02}"

Navigation through the weeks

Let’s add methods to go to the next and previous weeks:

def load_current_week(self):
    self.load_workouts(self.current_week_offset)

def show_previous_week(self):
    self.current_week_offset -= 1
    self.load_workouts(self.current_week_offset)

def show_next_week(self):
    self.current_week_offset += 1
    self.load_workouts(self.current_week_offset)

Logging workouts

We retrieved workouts, but we have no way to add them yet. Let’s add a method to save a workout to the database:

def log_workout(self):
    with rx.session() as session:
        workout = Workout()
        session.add(workout)
        session.commit()
    self.load_workouts(self.current_week_offset)

Note that I don’t have to specify any fields to Workout(), because theΒ completedΒ field (column) is set to the current datetime by default.

Building the UI

TheΒ StateΒ class has all we need from a back-end perspective. Now let’s build the front-end defining some UI elements. I define a couple of functions that return a rx.Component each.

You can also do this inline in the index() function (up next) but this increases the code’s readability and makes those components reusable.

def progress_display() -> rx.Component:
    return rx.vstack(
        rx.text(f"Workouts Completed {State.yymm}:", size="4"),
        rx.progress(value=State.progress_percentage)
    )

def week_navigation_buttons() -> rx.Component:
    return rx.hstack(
        rx.button("Previous Week", on_click=State.show_previous_week, size="2"),
        rx.button("Next Week", on_click=State.show_next_week, size="2"),
        spacing="4",
    )

def conditional_workout_logging_button() -> rx.Component:
    return rx.cond(
        State.goal_reached,
        rx.text("Congrats, you hit your weekly goal πŸ’ͺ πŸŽ‰", size="4", color="green"),
        rx.cond(
            State.current_week,
            rx.button(
                "Log Workout",
                on_click=State.log_workout,
                size="4",
                background_color="green",
                color="white",
            ),
            rx.text("", size="4"),
        ),
    )

def workout_list() -> rx.Component:
    return rx.vstack(
        rx.foreach(
            State.workouts, lambda workout_date: rx.text(f"Workout done: {workout_date}")
        ),
    )
  • progress_display()Β shows the progress towards the weekly goal. Note that I can accessΒ StateΒ properties directly.
  • week_navigation_buttons()Β shows buttons to navigate to the previous or next week, these call methods on theΒ StateΒ class.
  • conditional_workout_logging_button()Β accounts for several states: if the goal is reached, if we are in the current week, or if we can log a workout. Note that you need to useΒ rx.condΒ to conditionally render components (notΒ ifΒ statements).
  • workout_list()Β shows a list of workouts for the current week. Again we cannot use a plainΒ forΒ loop, the framework quickly taught me (through its error messaging) that I needed to useΒ rx.foreach (ChatGPT didn’t know)

Putting all the UI components together

Now we can update theΒ index()Β function to call our components in sequence:

def index() -> rx.Component:
    return rx.vstack(
        rx.heading("Fitness Tracker", size="9"),
        progress_display(),
        rx.heading("Workout History", size="7"),
        week_navigation_buttons(),
        workout_list(),
        conditional_workout_logging_button(),
        align="center",
        spacing="4",
    )

Pre-loading data upon app start

The page was already registered in the boilerplate code, but we did not give it a title so I did this here, as well as pre-loading the current week’s data using theΒ on_loadΒ keyword argument:

app = rx.App()
app.add_page(
    index,
    title="Fitness Tracker",
    on_load=State.load_current_week,
)

And that should be it, a full-stack web app with persistence and without writing any Javascript!

Result

Run uv run reflex run again and here is our little fitness app:

Screenshot 2025 01 15 at 18.20.30

Logging a workout we see our nice progress bar progress 20%:

Screenshot 2025 01 15 at 18.20.35

Logging more workouts, we’re at 80% of the week’s goal now (I actually hit that today πŸ’ͺ):

Screenshot 2025 01 15 at 18.20.39

Hitting the weekly goal the log workout button disappeared and we see a nice message:

Screenshot 2025 01 15 at 18.20.41

Going to the previous and next weeks there is nothing in the database yet and I should not be able to log anything (simplistic first MVP):

Screenshot 2025 01 15 at 18.20.43
Screenshot 2025 01 15 at 18.20.46

Here is the code for this project so you can try it out yourself.

Improvements

This is quite a simplistic app of course. Here are some ways to make it more functional:

  1. Being able to change the goal from 5 to something else. This is actually easy to accomplish adding an input field: rx.input(placeholder="Set goal", on_blur=State.set_target, size="3") -> set_attribute changes the attribute on the fly and the State class just flies with the change. I tried this and I did not have to make any other changes (similar how Excel formulas just re-calculate). πŸ“ˆ
  2. Ideally you should be able to set the date manually so you can still log workouts for previous weeks.
  3. I am excited to add a nice bar chart to see the performance over the weeks.

If you want to contribute to this repo, you’re more than welcome. Go here.

Note about learning approach

I used the following approach to come up with this app:

  1. I spent ~20 minutes looking at the get started docs + another 10-20 minutes on YouTube to see how databases worked with the framework (great complete app walk-through < I went straight to the sqlmodel part to learn how to use a DB).
  2. Then I used ChatGPT to come up with a quick prototype. It made more mistakes than usual, probably because this is a relatively new framework so there was less data in its training set. But I debugged my way through it learning the framework from the inside out.
  3. Then I went back to the docs and a lot of things made much more sense because I had used it.

This JIT learning (and dropping tutorial paralysis) style really works for us and it makes people that we coach very effective too.

What’s next?

I feel I have only scratched the surface. I am excited about this tool, because one advantage over say Streamlit is that it’s easier to customize and it seems to handle state changes better.

Of course I will stick with Django + htmx + Tailwind CSS (what we used for our v2 platform) for more involved web apps, but this framework looks really promising to quickly make web apps while having a certain degree of control.

Also notice I built something from scratch in this article. I cannot wait to play with the other templates presented in the init step, for example the dashboard one …


Thanks for reading. I hope this practical guide helps you get started with Reflex.

Let me know what you will build, and what your experience with Reflex has been …

Want a career as a Python Developer but not sure where to start?