Social LiveDocumentationPLATFORM DOCS

Mobile App Architecture

How the Flutter app is organized -small feature modules plugged into a shared shell.

Last updated Thu Jul 16 2026 00:00:00 GMT+0000 (Coordinated Universal Time)

TL;DR -The phone app is built from small, self-contained "modules" (wallet, chat, live room, and so on) that each plug into a shared app shell. Strict layering rules keep features from tangling into each other, and money/auth decisions always live on the backend, never in the app.

Who this is for -Product & business: the first two sections tell you how the app is shaped and why it stays maintainable. Mobile & engineering: everything, including the module contract, dependency rules, and testing conventions.

The Social Live phone app is written in Flutter (one codebase that ships to both iOS and Android). Instead of one giant program, it is assembled from many small pieces called modules -think of them like plug-in cartridges. There's a wallet module, a chat module, a live-room module, and so on. Each one is responsible for a single slice of the product and knows as little as possible about the others.

Why build it this way? Because a big app that everyone edits at once turns into a knot no one can safely change. Small modules with clear boundaries let different people work on different features without stepping on each other, and let you reason about (or delete) one feature without touching the rest.

In plain terms

The whole app is composed from three kinds of code:

  • The shell -the frame that holds everything: the bottom tab bar, the startup sequence, and the master list of which modules are switched on.
  • Core and shared infrastructure -plumbing that every feature reuses (the thing that talks to the server, the color palette, the reusable buttons and cards). This code knows nothing about any specific feature.
  • The feature modules -the actual product: wallet, chat, calls, live rooms, profile, search, and the rest.

The governing rule is simple: the shared plumbing never reaches up into features, and features never secretly wire into each other's guts -they only use each other's official "front door." And any rule that involves money or identity is decided by the backend server, never by the app on the phone. The app asks; the server decides.

How it works

The app (lib/) uses a Prism-style modular composition: small feature modules contribute routes and services to an app shell, with strict layering rules.

Layout

The folder structure mirrors that three-part split -shell (app/), plumbing (core/, shared/), and features (modules/):

lib/
  main.dart            entry point: system UI config, bootstrap, run app
  app/                 composition root
    app_bootstrap.dart   initializes registered modules
    app_shell.dart       navigation shell (bottom tabs)
    module_registry.dart the list of active modules
    routes/app_router.dart  GoRouter composed from module routes
  core/                app-wide infrastructure (no feature knowledge)
    config/              AppConfig (API base URL, dart-defines)
    network/             ApiClient (REST), error types
    utils/               logging, helpers
  modules/             feature modules (the real app)
    auth/ chat/ calls/ follow/ home/ live_room/ media_webrtc/
    notifications/ profile/ safety/ search/ signaling/ vip/ wallet/
  shared/              cross-feature UI kit (LiveColors, LiveDecorations, widgets)

The module contract

Every module presents the same tiny, standard "plug shape" so the shell can register it without knowing its internals. Each module exports an AppModule -deliberately tiny:

class AppModule {
  const AppModule({
    required this.name,
    this.routes = const [],   // GoRouter routes this module owns
    this.initialize,          // startup hook
    this.dispose,             // cleanup hook
  });
}

module_registry.dart lists active modules; AppRouter composes their routes. App-level files never import feature screens directly -a module's index.dart is its public API. (That index.dart is the "front door" mentioned above: everything else in the module is private.)

Inside a module

Every module is organized the same way internally, so once you've learned one you can navigate them all. The layers go from UI at the top down to dumb data at the bottom. Modules follow a consistent internal shape (see lib/modules/wallet/ as the reference example):

wallet/
  index.dart                     public exports
  wallet_module.dart             AppModule definition + routes
  application/                   controllers (StateNotifier) -logic layer
    coin_purchase_controller.dart
  data/                          repositories -API access + models
    wallet_repository.dart
  domain/models/                 pure model types (where present)
  presentation/screens|widgets/  UI only

Rule of thumb: screens render state and dispatch to controllers; controllers orchestrate; repositories talk HTTP; models are dumb. Business invariants that involve money or auth live on the backend -the client never decides them.

Dependency rules

These three rules are what keep the layering from eroding over time:

  • core/ and shared/ know nothing about features.
  • Modules may depend on core/, shared/, and other modules' index.dart exports (e.g. wallet reads the auth session provider for its bearer token).
  • app/ depends only on module registration surfaces.

Typography

One shared font keeps the app looking identical on both platforms. The app font is Inter (weights 400/500/600/700), bundled at assets/fonts/ and declared in pubspec.yaml; AppTheme (lib/app/theme.dart) sets it as the global fontFamily, so both platforms render identical type. Inter is SIL OFL licensed (assets/fonts/OFL-Inter.txt). Glyphs outside Inter's coverage (emoji, non-Latin scripts in user content) fall back to the platform font per-glyph. Live-room styled titles keep their own decorative TitleFontFamily options; their default option inherits Inter.

Testing

Tests are laid out to mirror the app, and the modules are built to be testable without a real phone or real network. test/ mirrors lib/ (test/modules/wallet/...). Controllers are designed for testability: external effects (HTTP, store plugins) sit behind injected seams -e.g. CoinPurchaseStore wraps in_app_purchase so purchase logic tests run without platform channels. Run everything with flutter test.