Skip to content

Architecture

aiserrock edited this page Jul 8, 2026 · 1 revision

Architecture

State management

  • flutter_bloc Cubit per screen/feature, no raw Bloc/events (except OnboardingBloc, a legacy holdout).
  • Screen state is (or embeds) a sealed DataState<T> (lib/util/data_state.dart): DataLoading<T>, DataContent<T>, DataError<T>. Immutable, Equatable-based, exhaustive switch.
  • DataStateBuilder<T> renders one of loader / builder / errorBuilder for a given DataState<T>. DataStateConsumer wraps it in a BlocBuilder for one-liner screens.
  • safeEmit (lib/util/bloc_ext.dart, extension on Cubit<S>) guards emit after await: if the Cubit is already closed (screen popped mid-request), it's a no-op instead of throwing StateError. Use safeEmit in every async Cubit method, never raw emit after an await.
  • Composite states use copyWith on a state class holding multiple DataState<T> fields (e.g. animal, prescriptions loaded independently) rather than one DataState for the whole screen.

Navigation

  • go_router, single GoRouter built by createAppRouter() (lib/navigation/app_router.dart) and registered as a singleton in DI — not a service/interface wrapper.
  • AppRoutes (abstract final class) holds path string constants plus small xPath(...) helpers for parameterized routes (path params/query params over passing raw objects where possible).
  • Complex non-primitive payloads go through extra, encoded/decoded by AppExtraCodec (lib/navigation/extra_codec.dart, GoRouter.extraCodec). Primitives and List/Map<String, Object?> pass through as-is; anything else is swapped for a one-time token backed by an in-memory store, so the Inspector/devtools never try to serialize live objects (models, callbacks). Tokens are consumed exactly once on decode — don't rely on extra surviving a rebuild/replay.
  • Screens returning a result use context.push<T>() / context.pop(result); auth flow (login → pick shelter → root) navigates explicitly via go/push rather than router redirects, since it's multi-step.

Dependency injection

  • get_it + injectable, code-generated (di_container.config.dart via @InjectableInit).
  • initDi() (lib/di/di_container.dart) is awaited first thing in main(), before runApp. It runs $initGetIt (env filter: prod unless annotated otherwise), then manually registers singletons that don't fit codegen: navigator/scaffold-messenger GlobalKeys and the GoRouter from createAppRouter().
  • Cubits are never registered in get_it. They're constructed directly and handed to a BlocProvider(create: (_) => XCubit(...)) at the screen/route level, scoped to that widget's lifetime.
  • Services, repositories, Dio/Chopper clients, storage wrappers are @injectable/@singleton/@module-annotated and pulled via getIt<T>() or constructor injection into Cubits.

Networking

  • Chopper (HTTP client codegen) on top of Dio (actual transport, interceptors, timeouts).
  • All API models/clients under lib/api/ (openapi.swagger.dart, openapi.enums.swagger.dart, *.swagger.chopper.dart, *.swagger.g.dart) are generated by swagger_dart_code_generator from an OpenAPI spec placed in doc/api/. Never hand-edit generated files.
  • The generator is currently enabled: false in build.yaml because no spec is checked into doc/api/ — generated output is committed as-is. Drop a spec in doc/api/ and flip the flag to regenerate.
  • Hand-written DTOs (non-API) use @JsonSerializable with a part '<name>.g.dart';.
  • Dio instance is built in DioRegister (lib/service/client/dio_register.dart, @module, timeouts only); auth/header behavior lives in separate interceptors (auth_interceptor.dart, header_inteceptor.dart) and auth_client_register.dart wires the authenticated Chopper client.

Storage

  • flutter_secure_storage for sensitive data (tokens), wrapped via SecureStorageRegister (lib/service/secure_storage/, @module), a memoized singleton FlutterSecureStorage. iOS KeychainAccessibility.first_unlock explicitly set; Android encrypts at rest by default.
  • shared_preferences for plain app prefs, provided by SharedPreferenceRegister (@module, @preResolve since SharedPreferences.getInstance() is async) and consumed through a thin typed wrapper, PreferenceStorage (lib/service/shared_pref/preference_storage.dart) — e.g. isFirstLaunch. Don't reach for SharedPreferences/FlutterSecureStorage directly from feature code; go through the lib/service/ wrapper.

Clone this wiki locally