← Blog
May 20265 min readreact-nativeexpomobileproductivity

Building FocusLock: why I made a focus timer that counts your failures

The reasoning behind FocusLock, the app architecture, and why I intentionally kept the product smaller and harsher than a typical productivity app.

FocusLock started from a simple frustration: most focus apps either try to become your entire productivity system or they pretend passive tracking is enough to change behavior. I did not want to build either of those.

The useful signal for me was much smaller. If I say I am going to focus for 25 minutes, how many times did I actually leave the app? That number is blunt, a little uncomfortable, and much harder to rationalize away than a streak animation or a nice-looking dashboard. That became the product.

The product decision that shaped the whole app

The main decision was to define the app around a single behavioral metric: interruptions. Not screen time. Not task completion. Not AI advice. Just whether you broke the session.

Once I committed to that, a lot of other decisions became obvious:

  • No task manager
  • No accounts
  • No backend
  • No notifications trying to drag you back in
  • No permissions beyond what the app already gets by existing in the foreground

That sounds restrictive, but it is what gave the product a clear shape. A focus timer is easy to make. A focus timer with a clean opinion is harder.

The development order

I built FocusLock from the center out.

The first thing that had to work was the session screen. If the timer was unreliable or interruption counting felt inconsistent, the whole app would collapse. So the early work was not about polish. It was about getting the session loop solid:

  • start a session with a duration and optional label
  • keep the device awake while the session is active
  • detect when the app leaves the foreground
  • finish cleanly into a summary screen

Only after that felt stable did I add the supporting surfaces: history, streaks, saved presets, and settings.

Why Expo Router and local storage were the right fit

This app does not need a backend, shared state library, or a complicated navigation model. It is basically a tight sequence of screens with a few persistent records attached to it. Expo Router was enough structure without becoming a second project.

The route split is very literal:

  • / for session setup
  • /session for the active timer
  • /summary for the result
  • /history for review
  • /settings for presets

That is the whole product. When the product shape is that simple, the architecture should admit it instead of hiding it under abstractions.

For persistence, I used AsyncStorage. Sessions, streak data, and duration presets all live on device. That let me keep the data model dead simple and avoid inventing sync or auth problems for an app that is primarily about private, personal behavior.

Counting interruptions without building a blocker

The core implementation detail is React Native's AppState API. During a running session, the app listens for transitions away from active. When that happens, the interruption count increments.

That is deliberately different from trying to block the phone or police every action. Blocking systems create an arms race with the user. Interruption counting creates a mirror. I trust the mirror more.

It also keeps the implementation honest. The app does not need special device privileges or unreliable background work. It just watches whether the session stayed in the foreground.

The other pieces that mattered

Two smaller decisions ended up being important.

The first was expo-keep-awake. A focus timer that lets the device sleep mid-session is a bad focus timer. Keeping the screen awake during the active run makes the product feel intentional instead of accidental.

The second was the scoring and streak logic. The score is simple on purpose: start at 100, subtract for interruptions, never go below zero. Completed sessions can advance your streak; sessions ended early get saved for history, but they do not count. I wanted the history to be honest without pretending an abandoned session was equivalent to a finished one.

Why I kept the UI severe

The visual design is dark, minimal, and a little unforgiving. That was intentional. I did not want the app to feel playful or gamified. The red fade that creeps in after repeated breaks is part of the product, not decoration. It turns the cost of distraction into something you feel immediately.

This is one of those cases where product and interface are tightly connected. If the app looked soft and celebratory no matter what happened, it would undercut the point.

What I would not add

I am careful about features that sound useful but would dilute the product:

  • task lists
  • pomodoro variations with too many settings
  • cloud sync
  • productivity graphs for their own sake

All of those could be justified. None of them would make FocusLock better at its core job, which is telling you whether you stayed with the session you said you were going to do.

That was the whole idea from the beginning: build the smallest version of a focus app that still tells the truth.