This document describes the current Flutter app architecture and the rules for growing it as routing, transit, backend integration, and AI trip planning are added.
Meander uses a feature-first Flutter structure based on Pragmatic Flutter Architecture:
- Services wrap external boundaries: HTTP APIs, platform services, map SDK helpers, persistence, and OS permissions.
- Managers own business workflows and expose reactive state and commands to UI.
- Views and widgets render UI, read manager state, and call manager actions.
The FastAPI backend is the main app boundary for production features. The Flutter app should prefer backend endpoints over direct calls to Valhalla, MOTIS, AI providers, or the database. Direct upstream clients can exist as development or low-level transport helpers, but product workflows should be modeled around backend-facing services.
app/lib/
main.dart
app/
app_config.dart
app_theme.dart
bootstrap.dart
meander.dart
_shared/
models/
services/
widgets/
features/
location/
manager/
model/
services/
map/
manager/
model/
pages/
services/
widgets/
routing/
manager/
model/
services/
transit/
manager/
model/
services/
trip_planning/
manager/
model/
services/
main.dart stays thin. It should only delegate to app bootstrap code.
app/ contains app-wide composition: startup, root widget, theme, constants,
routing setup when introduced, and future dependency registration entry points.
_shared/ is only for code used by multiple features. Do not put code there
preemptively. Start in a feature, then move to _shared/ when at least two
features need it.
features/ contains product areas. Each feature owns its models, services,
managers, pages, and widgets unless the code is genuinely shared.
pages/
: Full-screen or high-level feature views. Pages may own UI lifecycle objects
such as controllers from platform views. For example, MapLibreMapController
belongs in the map page layer because it is tied to the rendered map widget.
widgets/
: Feature-specific reusable UI pieces. Widgets should render state and call
callbacks or manager actions. They should not directly call HTTP services.
model/
: Domain objects, DTOs, request objects, response objects, and feature proxies.
Models should avoid importing Flutter UI packages unless the type is explicitly
UI or SDK-bound. For map models, LatLng is currently accepted because selected
targets are MapLibre-derived.
services/
: External and technical boundaries. Examples include Dio clients, MapLibre
style helpers, icon registration, location permission services, and backend API
services. Services should not own app state.
manager/
: Business workflows and state. Managers should coordinate services, expose
ValueListenable state, and provide commands for user-triggered async actions.
Managers are intentionally not one-to-one view models.
The app shell is split into:
app/bootstrap.dart: Flutter initialization and startup services.app/meander.dart: rootMaterialApp.app/app_config.dart: shared app constants.app/app_theme.dart: root theme construction.
The map feature is split into:
features/map/pages/map_shell.dart: MapLibre widget, map controller lifecycle, location callbacks, and page composition.features/map/model/rendered_map_feature.dart: normalized rendered feature data from MapLibre query results.features/map/model/selected_map_target.dart: selected POI or waypoint.features/map/services/map_feature_hit_tester.dart: nearest-feature logic.features/map/services/map_icon_catalog.dart: style icon catalog and feature-to-icon mapping.features/map/services/map_icon_registry.dart: SVG-to-PNG registration for MapLibre.features/map/services/map_selection_overlay.dart: selection circle and waypoint symbol operations.features/map/services/map_style_config.dart: map style IDs, layers, and initial camera constants.features/map/widgets/*: map panel, title badge, and location button.
Routing and transit are split into:
features/routing/model/valhalla_route_request.dartfeatures/routing/services/valhalla_client.dartfeatures/transit/model/motis_plan_request.dartfeatures/transit/services/motis_client.dart
These current clients still target upstream Valhalla and MOTIS directly. As
app workflows mature, add backend-facing services beside or in place of them,
for example RoutingApiService and TransitApiService.
Keep dependencies flowing inward and downward:
- Pages and widgets may depend on managers, models, and feature widgets.
- Managers may depend on services, models, and other managers when there is a clear domain relationship.
- Services may depend on transport packages and external SDKs.
- Models should stay mostly independent.
- Shared code must not depend on feature code.
- Feature code should not import another feature's private implementation
details. If cross-feature data is needed, move the common model to
_shared/or expose it through a manager/service boundary.
Avoid barrel files until imports become noisy. Direct imports make ownership clear while the app is still small.
The app is prepared for the flutter_it construction set, but the packages should be added incrementally:
- Add
get_itwhen dependency registration becomes useful. - Add
watch_itfor widgets that rebuild from manager state. - Add
command_itfor user-triggered async actions with loading and errors. - Use
listen_itfor side effects outside widgets.
Managers should expose ValueListenable state and commands. UI-triggered async
work should be represented as commands once command_it is introduced. Manager
init() methods may call services directly for initial data loading; commands
are for UI actions.
Do not put MapLibreMapController in global dependency injection. Keep it in
the map page or a page-owned adapter because its lifecycle belongs to the map
widget.
Use a single backend base URL configuration for app API calls, preferably via
--dart-define, for example:
--dart-define=QUESTMAP_API_BASE_URL=https://back.hack5.yandrik.devBackend-facing services should be feature-owned:
features/routing/services/routing_api_service.dartfor/routing/routefeatures/transit/services/transit_api_service.dartfor/transit/planfeatures/trip_planning/services/trip_planning_api_service.dartfor future AI trip-planning endpoints
The Flutter app should not call AI providers, SurrealDB, or routing engines directly in product flows. Those decisions belong in the backend.
When adding a feature:
- Create folders under
features/<feature>/. - Put request/response/domain objects in
model/. - Put HTTP or platform boundaries in
services/. - Put business workflows in
manager/. - Put screens in
pages/. - Put feature-specific UI pieces in
widgets/. - Add tests next to the existing test structure, named for the feature or unit under test.
Only promote code to _shared/ after reuse is real.
Keep tests focused on boundaries:
- Model tests for serialization, parsing, formatting, and validation.
- Service tests for request shape, endpoint paths, and error mapping.
- Manager tests with fake services for business workflows.
- Widget tests for panels, buttons, and state-specific rendering.
- Smoke tests for full pages where platform-view behavior is hard to unit test.
Run these before handing off app architecture changes:
cd app
dart format lib test
flutter analyze
flutter testNear-term architecture work should happen in this order:
- Introduce a backend
ApiClientand backend-facing routing/transit services. - Move location permission handling into
features/location. - Add
get_itregistration once services/managers need composition. - Add
MapSelectionManagerfor selected target and query state. - Add route/transit planning managers with commands.
- Add typed backend result models instead of passing raw JSON maps to UI. The main goal is to keep product workflows out of pages while keeping SDK controller lifecycle code close to the widgets that own it.