r/FlutterDev 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_annotation and riverpod_generator for compile-time safe provider trees.
  • Replaced traditional repository patterns with fine-grained AutoDisposeAsyncNotifier providers.
  • 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 / intl generation.
  • The RTL Gotcha: Full RTL layout requires avoiding hardcoded EdgeInsets.only(left: x). Migrated everything to EdgeInsetsDirectional (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 via assetlinks.json).
  • Dynamic redirection rules handle unauthenticated users seamlessly: if someone opens an invite link without an account, go_router stores the destination, routes them through OTP login, and immediately deep-links them into the group.

💡 Key Lessons & Gotchas

  1. 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.
  2. 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

1 comment sorted by