Just wrapped up a side project I've been chipping away at: uber-ui-practice, a Flutter recreation of Uber's rider app interface. It covers 8 screens, all built from scratch with mock data only.
Quick disclaimer: personal learning project, not affiliated with Uber in any way. No backend, no real APIs, all design rights belong to them.
What's inside
Splash, welcome, login, home, services, activity, an activity filter sheet, and the account tab. Here are four of them:
![]() |
![]() |
![]() |
The rest are in the repo README.
Structure: feature-first
I went with features instead of layers this time, and I'm never going back to layer-first for UI projects:
lib/
├── core/
│ ├── constants/ # auth strings (app is in French)
│ ├── theme/ # colors, type scale
│ └── widgets/ # buttons, inputs, dividers
├── features/
│ ├── auth/
│ │ ├── controller/
│ │ ├── data/
│ │ ├── models/
│ │ ├── pages/
│ │ └── widgets/
│ └── tabs/
└── main.dart
When a screen looks wrong, I know exactly which folder owns the problem. No jumping between a global widgets/ dump and three screens folders trying to trace who renders what.
Details that took real effort
Onboarding as a state machine. Splash, welcome and login are states inside a single AuthFlow widget rather than separate routes. After login, one pushAndRemoveUntil replaces the stack with the home shell, so back navigation can't leak into the auth flow:
void _goHome() {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => HomePage(onLogout: _logout)),
(route) => route.isFirst,
);
}
The floating pill nav. This little bar ate more time than any full screen. Tabs live in a PageView locked with NeverScrollableScrollPhysics, so only the pill switches pages. The pill waits ~250ms before sliding so it lands after the page transition instead of racing it, and the whole bar auto-hides on scroll down through UserScrollNotification.
Flag emojis with no assets. The country picker generates every flag at runtime from ISO codes:
final base = 0x1F1E6; // regional indicator A
String get flagEmoji => String.fromCharCodes(
isoCode.runes.map((r) => base + (r - 65)));
One theme file to rule the surfaces. Every color token sits in core/theme. When your goal is matching someone else's exact near-black, centralizing tokens is what keeps you sane.
The part nobody warns you about
Cloning a production design is stricter than designing your own. With your own app, close enough is fine. Here, every spacing value had a correct answer, and mine was often not it. Annoying sometimes, but honestly great training for design attention.
Links
Repo, with tests passing and setup docs:
👉 github.com/abidiahmedcom/uber-ui-practice
If this kind of UI study interests you, I applied the same approach to Glovo's delivery app as well: glovo-ui-practice.
Feedback welcome, and if you notice anywhere my version drifts from the original, issues are open.



Top comments (0)