State & Routing Conventions
How the app tracks live data (Riverpod) and moves between screens (GoRouter).
Last updated Thu Jul 16 2026 00:00:00 GMT+0000 (Coordinated Universal Time)
TL;DR -The app uses Riverpod to hold and share data (like your wallet balance) and GoRouter to move between screens and handle deep links. A small set of repeatable patterns covers almost everything, and a shared UI kit keeps every screen looking the same.
Who this is for -Design & product: the "In plain terms" and "UI kit" sections explain why screens feel consistent and always show a loading/error/content state. Mobile & engineering: the full provider patterns, routing table, and deep-link rules.
Two libraries do most of the heavy lifting in the app's day-to-day plumbing. Riverpod is how the app keeps track of live data and shares it between screens -for example, your wallet balance is fetched once and any screen that needs it reads the same copy. GoRouter is how the app knows which screen you're on and how tapping something takes you to the next one, including "deep links" (a link from outside the app that jumps you straight to a specific room or chat).
Rather than solving these problems a new way each time, the codebase leans on a few standard patterns. Learn the handful below and you can read almost any screen in the app.
In plain terms
Data in the app comes in three flavors, and each has a standard tool:
- A thing that talks to the server (a "repository") -rebuilt automatically when you log in or out, so screens never show a stale, logged-out view.
- A piece of data fetched for a screen (like your balance) -cached while you're looking at it, thrown away when you leave to keep memory low, and refreshed after you do something that changes it.
- A stateful flow (following someone, buying coins, running a live room) -handled by a "controller" that holds the current state and the logic for changing it.
For navigation, each feature owns its own set of screen addresses (URLs), and links from outside the app are cleaned up so they always land in the right place. And every screen that loads something shows one of three states -a loading placeholder, an error-with-retry, or the real content -using a shared kit of building blocks so the whole app feels like one product.
Riverpod patterns
Three provider shapes cover nearly everything:
1. Repository providers (plain Provider)
A repository is the object a feature uses to talk to the server. It's wired so it automatically rebuilds when the login session changes:
final walletRepositoryProvider = Provider<WalletRepository>((ref) {
return WalletRepository(
apiClient: const ApiClient(),
accessToken: ref.watch(authSessionProvider)?.accessToken,
);
});
Watching authSessionProvider means repositories rebuild automatically when
the session changes -login/logout invalidates the whole graph for free.
2. Data providers (FutureProvider.autoDispose)
These fetch a piece of data for a screen and cache it only while that screen is open:
final walletBalanceProvider = FutureProvider.autoDispose<WalletBalance>((ref) {
return ref.watch(walletRepositoryProvider).fetchWallet();
});
autoDispose keeps memory flat: leave the screen, the cache dies. Screens
refresh with ref.invalidate(...) after mutations.
3. Controllers (StateNotifierProvider.autoDispose)
These run the interactive, multi-step flows. Stateful flows (follows, notifications, live room, coin purchases) use
StateNotifier controllers holding an immutable state object. Controllers
receive dependencies through their constructor -never ref directly -so
tests construct them with fakes.
The purchase controller shows the full pattern: injected repository +
store seam + platformOverride for tests, an explicit state enum, and a
revision counter so the UI can distinguish repeated identical messages.
GoRouter
Every screen has an address, and each feature module owns its own addresses. Links from inside and outside the app both resolve through the same table:
- Routes are contributed by modules (
wallet_module.dartowns/wallet). - Route ownership map: auth
/,/login· home/home· live-room/live/create,/live/:roomId· chat/inbox/:threadId· vip/vip· wallet/wallet· safety/report/:targetId· profile, search, etc. - Navigation uses
context.push('/wallet/history')style paths; deep links resolve through the same table. Externalsociallive://links (share invites) are normalized by a top-level GoRouterredirect(normalizeDeepLinkinapp_router.dart) that folds a non-empty URI host back into the path, sosociallive://live/xandsociallive:///live/xboth land on/live/x.
UI kit
A shared set of building blocks is what makes every screen look like it belongs to the same app. shared/widgets/live_ui.dart centralizes the visual language:
LiveColors (palette), LiveDecorations.embossedCard() (the signature
card), LiveGradientBackground, LiveSkeletonCard (loading),
LiveErrorState (error + retry). New screens compose these instead of
inventing styles -that is what keeps the app visually coherent.
Every async screen renders all three states: skeleton while loading,
LiveErrorState with retry on failure, content on success.
Related pages
- Mobile App Architecture -the module system these patterns live inside.
- Networking Layer -what repositories call to reach the server.
- Tokens, Sessions & Devices -the session that
authSessionProviderwatches.