cyclemind_ai/
An AI cycling companion that puts a coach and a bike doctor in the same Flutter app. Photograph a bike and a vision model returns a health report. The coach reads your rides and plans the next one.
Nobody reads your ride data, and nobody checks the bike
Two things stop a self-coached cyclist from improving. Nobody is reading their ride data and telling them what to do next, and nobody looks at the bike until something snaps. Both are judgement calls, and both are the sort of thing a model with the right context can help with.
Three decisions the whole app hangs on
This is the project where I took architecture seriously. It follows Clean Architecture with a feature-first layout and the repository pattern, and three decisions drive everything else.
-
Every external dependency sits behind an interface. Auth,
Firestore, Storage, text AI and vision are each reached through a Dart
abstract interface class. Mock or real gets chosen in one place per dependency, by a Riverpod provider reading a single flag. That is dependency inversion doing something useful: the entire app runs offline, on mock data, with no API keys and no setup. - The API key never ships in the app. Real AI and vision calls are made by Cloud Functions that hold the key server-side, and the Flutter client only POSTs to those endpoints. A mobile binary is not a place you can keep a secret.
-
Errors never leak across layers. Data sources throw.
Repositories catch and return a
Result<T>wrapping a typed failure. The interface folds over the result. No exception crosses a layer boundary, so no screen has to guess what went wrong.
There is no code generation either. Hand-written immutable models and hand-written
providers, instead of freezed, json_serializable and riverpod_generator. It
compiles with a plain flutter pub get, with no build_runner step to
go stale.
Six features, each behind its own interface
On main, the branch the source link below points at:
- auth: email and password sign-in, Google sign-in, profile creation.
- dashboard: readiness, weekly stats, AI recommendations and bike health at a glance.
- coach: rides, trends, goals, a training plan and a readiness score.
- bike_doctor: photo, vision analysis, health report, then a chat with a virtual mechanic.
- bikes: multiple bikes, components, mileage and maintenance reminders.
- profile: goals and preferences.
These are captured from the build its own GitHub Actions workflow publishes, not from a simulator. There is no Flutter SDK where I write from, so the way to see the app is to run the artefact CI produced. That is the mock-first rule paying for itself: no keys, no backend, no setup, and the app still runs.
One mismatch worth naming rather than hiding: the demo is published from a
branch ahead of main, where features/coach has been
replaced by features/rides. So the screenshot shows four tabs and
no Coach, while the list above, and the source link, still describe
main.
Mock-first, or the architecture is wrong
Material 3, go_router and Riverpod throughout. The rule I want to carry into every future project is the mock-first one: if somebody cannot clone the repo and see the app work in a single command, the architecture is wrong.