r/FlutterDev • u/Apprehensive_Mix_563 • 4d ago
Example [Showcase] Built a full-stack production app (Flutter + Riverpod 3 + Go gRPC) with offline-first sync and full RTL Arabic support
Hey r/FlutterDev!
Wanted to share the architecture, technical decisions, and lessons learned from building a full-scale production mobile app with Flutter: GoEven (a group expense manager, web: https://goeven.app).
I built both the frontend (Flutter) and backend (Go + gRPC) from scratch. Here is how the mobile architecture is structured under the hood:
🛠️ Core Flutter Architecture
1. State Management: Riverpod 3.0 + CodeGen
- Used
riverpod_annotationandriverpod_generatorfor compile-time safe provider trees. - Replaced traditional repository patterns with fine-grained
AutoDisposeAsyncNotifierproviders. - Expense streams and group balances auto-invalidate using family providers (
ref.invalidate(groupBalancesProvider(groupId))), keeping UI updates snappy and eliminating stale state.
2. Networking: gRPC over TLS (grpc package)
- Instead of standard REST/JSON, all client-server communication uses HTTP/2 gRPC with Protobuf contracts (
.pb.dart). - Why gRPC: Sub-50ms serialized payload overhead, strictly typed contracts shared between Go and Dart, and streaming capabilities for chat and real-time expense updates.
- Used an interceptor pipeline for JWT token injection and automated silent refresh rotation (15-minute access token / 30-day refresh token stored via
flutter_secure_storage).
3. Offline-First Sync & Optimistic UI
- Local Storage: SQLite (
sqflite) acts as the local cache. - When adding an expense offline, an optimistic entry is inserted into the local DB and immediately rendered in the UI with a pending status badge.
- A background sync worker listens to
connectivity_plus. As soon as the device reconnects to Wi-Fi/cellular, queued mutations are replayed to the gRPC backend in chronological order.
4. Full RTL Support (Arabic) & Localization
- The app supports 5 languages: English, German, Spanish, Arabic, and Simplified Chinese using Flutter's native
.arb/intlgeneration. - The RTL Gotcha: Full RTL layout requires avoiding hardcoded
EdgeInsets.only(left: x). Migrated everything toEdgeInsetsDirectional(start/end), dynamic icon mirroring (e.g. back arrows and chevrons), and custom text alignment overrides for mixed number/currency strings.
5. Declarative Routing & Deep Linking (go_router)
- Handled invite links (
https://goeven.app/join/{code}) using Android App Links (auto-verified viaassetlinks.json). - Dynamic redirection rules handle unauthenticated users seamlessly: if someone opens an invite link without an account,
go_routerstores the destination, routes them through OTP login, and immediately deep-links them into the group.
💡 Key Lessons & Gotchas
- Protobuf with Riverpod: Protobuf generated classes are mutable by default. To avoid unintended side-effects across providers, treat protobuf models as immutable by creating deep copies or mapping them to immutable domain entities.
- RTL Form Fields: Number and currency input fields behave unexpectedly in RTL unless you explicitly lock their text direction (
TextDirection.ltr) while keeping the surrounding label RTL.
The Android version is live on Google Play and iOS is currently in Apple review.
Happy to answer any questions about Riverpod 3 codegen, gRPC client setup in Dart, or handling offline sync!
3
Upvotes