Skip to main content

Architecture Philosophy

Invenicum follows a layered architecture pattern that separates concerns into distinct layers:
  • Presentation Layer: UI components (Screens & Widgets)
  • Business Logic Layer: State management (Providers)
  • Data Layer: Services and Models
  • Infrastructure Layer: Routing, localization, configuration
This separation ensures maintainability, testability, and scalability as the application grows.

Core Architecture Patterns

Three-Layer Architecture

┌─────────────────────────────────────┐
│     Presentation Layer              │
│  (Screens, Widgets, UI)             │
└──────────────┬──────────────────────┘
               │ uses
┌──────────────▼──────────────────────┐
│    Business Logic Layer             │
│  (Providers - State Management)     │
└──────────────┬──────────────────────┘
               │ uses
┌──────────────▼──────────────────────┐
│        Data Layer                   │
│  (Services, Models, API Client)     │
└─────────────────────────────────────┘

Provider Pattern for State Management

Invenicum uses the Provider package for state management, implementing:
  • ChangeNotifier for reactive state updates
  • ChangeNotifierProvider for independent state
  • ChangeNotifierProxyProvider for dependent state (e.g., auth-dependent providers)
The provider hierarchy is established in lib/main.dart:64-261, where services are initialized first, followed by state providers.

Service Layer Pattern

All backend communication is abstracted through service classes:
  • ApiService (lib/data/services/api_service.dart): Base HTTP client with Dio
  • Domain Services: Specialized services for each domain (InventoryItemService, ContainerService, etc.)
Services are provided as singletons through Provider<T> (not ChangeNotifierProvider) since they don’t manage reactive state.

Key Architectural Components

1. API Client Architecture

The ApiService class uses the singleton pattern and provides:
// lib/data/services/api_service.dart:8-53
class ApiService {
  static final ApiService _instance = ApiService._internal();
  final Dio dio = Dio();
  String? _cachedToken;  // In-memory token cache
  
  factory ApiService() => _instance;
}
Key features:
  • Synchronous token caching for performance (_cachedToken)
  • Dio interceptors for automatic auth header injection
  • Global error handling (401 auto-logout)
  • Configurable timeouts via Environment config

2. Authentication Flow

┌──────────┐     login()      ┌──────────────┐
│  User    │ ─────────────────>│ AuthProvider │
└──────────┘                   └──────┬───────┘

                              ┌───────▼────────┐
                              │  ApiService    │
                              │  POST /login   │
                              └───────┬────────┘

                              ┌───────▼────────────┐
                              │  Token Stored:     │
                              │  - Memory cache    │
                              │  - SharedPrefs     │
                              └───────┬────────────┘

                              ┌───────▼────────────┐
                              │ ProxyProviders     │
                              │ Auto-initialize    │
                              └────────────────────┘
Implementation: lib/providers/auth_provider.dart:293-319
  1. User credentials sent to ApiService
  2. Token cached in memory and persisted to SharedPreferences
  3. refreshListenable on GoRouter triggers route guards
  4. ChangeNotifierProxyProviders react to auth state changes

3. Routing Architecture

Invenicum uses GoRouter with declarative routing:
// lib/core/routing/app_router.dart:58-99
GoRouter createAppRouter(AuthProvider authProvider) {
  return GoRouter(
    refreshListenable: authProvider,  // Re-evaluate on auth changes
    redirect: (context, state) {
      // Authentication guards
      // QR code deep-link handling
      // Session persistence
    },
    routes: [
      // Login (outside ShellRoute)
      // Protected routes (inside ShellRoute with MainLayout)
    ]
  );
}
Key features:
  • Auth-based redirects
  • Deep linking support (QR codes)
  • ShellRoute for consistent layout
  • Path parameter extraction for nested resources

4. Plugin System Architecture

The plugin system allows dynamic UI injection using STAC (Server-Driven UI):
┌─────────────────┐
│  GitHub Repo    │  Plugin definitions stored as JSON
└────────┬────────┘

    ┌────▼─────────────┐
    │ PluginService    │  Downloads and manages plugins
    └────┬─────────────┘

    ┌────▼─────────────┐
    │ PluginProvider   │  State management for plugins
    └────┬─────────────┘

    ┌────▼─────────────┐
    │ Stac.fromJson()  │  Renders plugin UI dynamically
    └──────────────────┘
Implementation files:
  • lib/data/services/plugin_service.dart: API integration
  • lib/providers/plugin_provider.dart: Plugin state management
  • lib/core/utils/sdk_plugin_parser.dart: STAC action parser
Plugin slots: Widgets can define slots using StacSlot (e.g., inventory_header in lib/widgets/layout/main_layout.dart:340)

5. State Dependency Chain

Many providers depend on authentication state:
// lib/main.dart:171-185
ChangeNotifierProxyProvider<AuthProvider, InventoryItemProvider>(
  create: (c) => InventoryItemProvider(
    c.read<InventoryItemService>(),
    c.read<AssetPrintService>(),
  ),
  update: (context, auth, prev) {
    if (!auth.isLoading && auth.isAuthenticated && auth.token != null) {
      if (prev != null && !prev.isLoading) {
        Future.microtask(() => prev.loadAllItemsGlobal());
      }
    }
    return prev!;
  },
)
This pattern ensures:
  • Data loads only when authenticated
  • Prevents redundant API calls
  • Automatic cleanup on logout

Data Flow Example

Let’s trace a typical operation: Loading inventory items
1. Screen initiates load
   AssetListScreen → context.read<InventoryItemProvider>()
   
2. Provider processes request
   InventoryItemProvider.loadInventoryItems() → checks cache
   
3. Service makes API call
   InventoryItemService.fetchInventoryItems() → ApiService
   
4. ApiService executes HTTP
   dio.get('/items') with auto-injected auth header
   
5. Data flows back
   JSON → Model.fromJson() → Service → Provider
   
6. UI rebuilds
   Provider.notifyListeners() → Consumer rebuilds
Code references:
  • Screen: lib/screens/assets/asset_list_screen.dart
  • Provider: lib/providers/inventory_item_provider.dart:292-346
  • Service: lib/data/services/inventory_item_service.dart

Component Interactions

MainLayout Integration

The MainLayout widget (lib/widgets/layout/main_layout.dart) demonstrates full-stack integration:
// Combines:
- Sidebar navigation (SidebarLayout)
- Header with search, notifications, profile
- Dynamic plugin slots (StacSlot)
- AI chatbot (VeniChatbot)
- Route-based hydration (deep-link support)
Hydration pattern (lib/widgets/layout/main_layout.dart:61-107):
  1. Parse URL on layout mount
  2. Extract container/asset type IDs
  3. Pre-load data into providers
  4. UI renders with data ready

Configuration & Environment

Configuration is centralized in lib/config/environment.dart:
class Environment {
  static const String apiUrl = 'https://api.example.com';
  static const String apiVersion = '/v1';
  static const int connectTimeout = 30000;
  static const int receiveTimeout = 30000;
  static const String authTokenKey = 'auth_token';
}

Localization Architecture

Invenicum supports multiple languages using Flutter’s built-in l10n:
  • Definitions: lib/l10n/ (ARB files)
  • Access: AppLocalizations.of(context)
  • Locale management: PreferencesProvider
  • Configuration: lib/main.dart:298-304

Best Practices

  1. Separation of Concerns: Keep UI logic in widgets, business logic in providers, API calls in services
  2. Single Responsibility: Each provider manages one domain (auth, inventory, containers, etc.)
  3. Dependency Injection: Services injected via Provider, not instantiated in widgets
  4. Reactive State: Use context.watch<T>() for reactive rebuilds, context.read<T>() for one-time access
  5. Error Handling: Catch exceptions in providers, rethrow to UI for user feedback
  6. Memory Management: Dispose controllers, prevent memory leaks in providers

Next Steps

Build docs developers (and LLMs) love