This document describes the key architectural decisions made in LogiFlow Mobile, the reasoning behind them, and how the main systems work.
The application follows the Model-View-ViewModel (MVVM) pattern using CommunityToolkit.Mvvm.
- Native pattern for .NET MAUI and WPF applications
- Clear separation between UI logic (ViewModel) and presentation (View)
- Enables unit testing of all business logic without UI dependencies
- Data binding reduces boilerplate code
- Official Microsoft toolkit, actively maintained
- Source generators for
[ObservableProperty]and[RelayCommand]eliminate boilerplate BaseViewModelprovidesIsBusy,HasErrorandErrorMessageshared across all ViewModels- No runtime reflection, uses compile-time source generation
All services are registered in MauiProgram.cs using the built-in .NET DI container.
| Service | Lifetime | Reason |
|---|---|---|
ILogService |
Singleton | Single logger instance throughout the app |
ILocalizationService |
Singleton | Shared culture state |
INavigationService |
Singleton | Single navigation stack |
IAuthService |
Singleton | Stateless, safe to share |
ISettingsService |
Singleton | Cached settings |
ISessionService |
Singleton | Shared session state |
IReceptionService |
Singleton | Stateless, safe to share |
IMasterDataService |
Singleton | Stateless master data |
IReceptionSessionService |
Singleton | Shared reception flow state |
ICameraScanService |
Singleton | Shared scan callback coordination |
IClaudeService |
Singleton | Stateless HTTP client wrapper |
IChatDialogService |
Singleton | Resolves ChatViewModel from DI and shows popup |
ChatViewModel |
Transient | Fresh instance per chat session |
| ViewModels | Transient | Fresh instance per page |
| Pages | Transient | Fresh instance per navigation |
Custom INavigationService with a page registry dictionary instead of Shell navigation.
- Shell adds complexity for simple linear navigation flows
- Custom service is fully testable with
INavigationWindowServiceabstraction - Explicit page registration makes navigation dependencies visible
- Pages are registered in
NavigationServiceconstructor with aDictionary<string, Type> NavigateToAsync(pageKey)resolves the page from DI and pushes it to the navigation stackclearStack: trueinserts the new page before the root and pops to root — avoids back navigation to login after logoutINavigationWindowServiceabstractsNavigationPagefor unit testing
Centralized IErrorHandlerService with custom domain exceptions.
| Exception | When used |
|---|---|
ValidationException |
Invalid field values (URL, timeout) |
AuthException |
Authentication failures |
ConnectionException |
Server connectivity failures |
ViewModel → catches exception → IErrorHandlerService.Handle() → returns localized message → ViewModel sets ErrorMessage
- Single place to change error message format
- Consistent logging for all error types
- ViewModels stay clean — no inline error formatting
Custom TranslateExtension XAML markup extension with real-time language switching.
ILocalizationServicemanages the currentCultureInfoand firesLanguageChangedeventTranslateExtensioncreates aTranslationWrapperper bindingTranslationWrappersubscribes toLanguageChangedand implementsINotifyPropertyChanged- When language changes, all bindings update automatically without page reload
- Built-in localization requires app restart to change language
- Custom solution enables real-time switching from Settings screen
<Label Text="{local:Translate Key=SettingsTitle}" />_localizationService.GetString(nameof(AppResources.ErrorInvalidUrl))Serilog with file sink and Android Logcat sink.
| Level | When used |
|---|---|
Debug |
Detailed diagnostic information |
Info |
Normal application flow |
Warning |
Unexpected but recoverable situations |
Error |
Failures that need attention |
_logService.Info("User authenticated. Username={Username}", username);
_logService.OperationStart("Login", username);
_logService.OperationSuccess("Login", username);
_logService.OperationFailure("Login", username, reason);- Android:
files/logs/logiflow-YYYYMMDD.login app data directory - All platforms:
logs/logiflow-.logwith daily rolling - Accessible via:
adb exec-out run-as com.companyname.logiflow.mobile cat /data/data/com.companyname.logiflow.mobile/files/logs/logiflow-YYYYMMDD.log > C:\logs\logiflow.log
ISettingsService with JSON serialization stored via IPreferencesService (MAUI Preferences abstraction).
Preferencesis a MAUI platform API not available in unit testsIPreferencesServicewrapper enables full testing ofSettingsService
SettingsViewModel → ISettingsService → IPreferencesService → MAUI Preferences → Device storage
xUnit + Moq with 85% code coverage.
- All ViewModels — navigation, validation, error handling, loading states
- All Services — business logic, error paths, logging calls
- Converters and Extensions — edge cases
- XAML Pages (code-behind) — depend on
InitializeComponent(), not testable without UI - Platform wrappers (
PreferencesService,NavigationWindowService,FileSystemService) — thin wrappers over platform APIs MauiProgram,App,AppShell— infrastructure, no business logicThemeService.ApplyThemeToApplication— depends onApplication.Current, platform API
INavigationWindowServiceabstractsNavigationPagefor navigation testingIPreferencesServiceabstracts MAUIPreferencesfor settings testingIFileSystemServiceabstractsFileSystemfor log path testingICameraScanServicecoordinates scan callbacks between ViewModels and camera pageIChatDialogServiceabstracts popup display for AI chat testing
Centralized XAML styles in Resources/Styles/ following a Design System defined for industrial environments.
| File | Contents |
|---|---|
Colors.xaml |
Color palette |
Typography.xaml |
Font styles |
Buttons.xaml |
Button styles |
Entries.xaml |
Input field styles |
Labels.xaml |
Label styles |
Layout.xaml |
Card and section styles |
MenuStyles.xaml |
Menu item styles |
- Minimum touch target 48px for industrial use with gloves
- Every state communicated with icon + color + text, never color alone
- Maximum one Primary button per screen
Runtime dark/light theme switching using swappable ResourceDictionary instances.
| File | Contents |
|---|---|
LightColors.xaml |
Light theme color palette |
DarkColors.xaml |
Dark theme color palette |
Both files expose identical semantic keys so all styles resolve correctly in either theme.
IThemeService.ApplyTheme(code)removes the active color dictionary fromApplication.Current.Resources.MergedDictionariesand adds the new one- All style properties use
DynamicResource— controls update automatically without page reload - On startup,
SplashViewModel.StartAsync()readsSettings.TemaVisualand callsApplyTheme()before navigation - Theme selection is persisted via
ISettingsServicealongside other user settings
AppThemeonly supports system-level light/dark, not user-controlled switching- Custom solution gives full control over colors and allows the same pattern as
ILocalizationService
Entryplaceholder color usesPlaceholderColor="{DynamicResource SecondaryText}"inPrimaryEntryStyleEntryunderline, cursor, and text selection colors on Android are managed byEntryThemeUpdater(inPlatforms/Android/) viaEntryHandler.Mapper.AppendToMappinginMauiProgram.csApp.xaml.cssubscribes toIThemeService.ThemeChangedand traverses the visual tree to update all active Entry controls in real-timeEntryfields are wrapped inEntryBorderStyleborders for consistent theming of background and stroke colorsFontImageSourceelements useDynamicResourcefor real-time theme-aware color updates- Icons using
AppIconExtensionresolve color once on page creation — correct at startup, update when navigating to a new page
- Semantic keys only — no hardcoded hex values in style files
- New semantic keys added:
PageBackground,CardBackground,EntryBackground,ErrorEntryBackground - All pages set
BackgroundColor="{DynamicResource PageBackground}"explicitly
BarcodeScanning.Native.Maui with a dedicated CameraScanPage and ICameraScanService callback coordination.
- Native ML APIs on Android (Google ML Kit) and iOS (Apple Vision)
- Compatible with .NET MAUI 9
- Better performance and reliability than ZXing.Net.MAUI for .NET 9
- Any ViewModel that needs a scan calls
ICameraScanService.RequestScan(callback)and navigates toCameraScanPage CameraScanPageactivates the camera onOnAppearingand requests camera permission at runtime- When a barcode is detected,
CameraScanViewModelnavigates back first, then callsICameraScanService.DeliverResult(code) - The callback registered by the requesting ViewModel is invoked with the scanned code
- Navigation back happens before delivery to ensure the requesting page is active when the callback fires
- ViewModels are Singleton — the callback always targets the correct instance
- Delivering after navigation back ensures the UI binding updates are processed on the active page
- Prevents race conditions between navigation stack changes and property updates
- Android:
android.permission.CAMERAinAndroidManifest.xml+ runtime request viaPermissions.RequestAsync<Permissions.Camera>()
Multi-step sequential flow with IReceptionSessionService as shared state container.
ReceptionStartPage → ReceptionHeaderPage → ReceptionDetailPage → [ReceptionChecklistPage] → ReceptionConfirmationPage → ReceptionItemsPage
- The reception DTO is built progressively across 6 screens
- Singleton service preserves state across the entire flow
- Each ViewModel reads and writes to the session independently
- No coupling between ViewModels — they only depend on the service
| Type | Checklist | Extra fields |
|---|---|---|
| STANDARD | Not required | None |
| TEST_SAMPLE | Mandatory | Sample reference, lot number |
| Any with VEHICLE article | Mandatory | None |
Simulated receptions available for testing:
| Code | Flow type | Sender |
|---|---|---|
| REC-001 | STANDARD | Supplier A |
| REC-002 | TEST_SAMPLE | Lab B |
| REC-003 | STANDARD | Supplier C |
Anthropic Claude API (claude-haiku-4-5) integrated as a contextual WMS assistant available on all Reception screens.
BaseViewModel.OpenAiChatCommand
↓
IChatDialogService.ShowAsync(WmsScreenContext)
↓
ChatDialogService → resolves ChatViewModel from DI
↓
ChatBottomSheet (Popup) bound to ChatViewModel
↓
IClaudeService.SendAsync(history, context)
↓
Anthropic HTTP API → claude-haiku-4-5
IChatDialogService — decouples ViewModels from UI. ViewModels call ShowAsync(context) without knowing anything about Popups or Pages.
BaseViewModel.OpenAiChatCommand — the command is defined once in BaseViewModel. Any ViewModel that sets ChatDialogService and overrides GetAiContext() automatically gets the 💬 Ask AI button wired up via XAML binding with zero additional code-behind.
WmsScreenContext — each ViewModel overrides GetAiContext() to provide real-time screen state to Claude: reception number, flow type, current article, lines count, checklist progress, etc. Claude uses this as its system prompt context.
DataTemplateSelector — chat bubbles use ChatMessageTemplateSelector instead of IsVisible converters to avoid double-rendering of user and assistant messages.
ChatViewModel as Transient — fresh instance per chat session. History is cleared on InitialiseContext(), so each popup open starts a clean conversation with the current screen context.
| Environment | Source |
|---|---|
| Local debug | appsettings.Development.json (excluded from git) |
| CI/CD | GitHub Secret ANTHROPIC_API_KEY injected into appsettings.json at build time |
All 6 Reception screens expose OpenAiChatCommand via their ViewModel and show the 💬 Ask AI button in the page header.
Settings is a technical configuration screen used rarely by operators. There is no operational workflow context where Claude can add value. The AI assistant is intentionally scoped to operational modules only.
Follow these steps to add a new module (e.g. Reception):
1. Create the View
Views/Reception/ReceptionPage.xaml
Views/Reception/ReceptionPage.xaml.cs
2. Create the ViewModel
ViewModels/Reception/ReceptionViewModel.cs
Inherit from BaseViewModel and inject required services.
3. Register in MauiProgram.cs
builder.Services.AddTransient<ReceptionPage>();
builder.Services.AddTransient<ReceptionViewModel>();4. Register in NavigationService
{ nameof(ReceptionPage), typeof(ReceptionPage) }5. Add menu item in MenuViewModel
new(_localizationService.GetString(nameof(AppResources.MenuReception)), nameof(ReceptionPage), AppIconGlyph.Inventory2)6. Add localization keys to .resx files
<data name="MenuReception"><value>Reception</value></data>7. Add 💬 Ask AI button to XAML header
<Button Text="{local:Translate Key=AiChatAskButton}"
Command="{Binding OpenAiChatCommand}" ... />8. Write unit tests
Tests/ViewModels/ReceptionViewModelTests.cs
LogiFlow Mobile — Daniel Ojeda Ubeda