Documentation Index
Fetch the complete documentation index at: https://mintlify.com/lopiv2/invenicum/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Invenicum uses the Provider package for state management, implementing the ChangeNotifier pattern. This approach provides:
- Reactive UI updates
- Clear separation of business logic and presentation
- Dependency injection
- Testability
Provider Hierarchy
The complete provider tree is established in lib/main.dart:64-261:
runApp(
MultiProvider(
providers: [
// 1. Services (singletons)
// 2. State providers (ChangeNotifier)
],
child: const MyApp(),
),
);
Two-Tier Structure
Tier 1: Services (Provider)
Services are stateless singletons that handle API communication:
// lib/main.dart:66-83
Provider<ApiService>(create: (c) => ApiService()),
Provider(create: (c) => PluginService(c.read<ApiService>())),
Provider(create: (c) => DashboardService(c.read<ApiService>())),
Provider(create: (c) => InventoryItemService(c.read<ApiService>())),
// ... more services
Key characteristics:
- No reactive state (don’t call
notifyListeners())
- Injected via constructor dependency
- Single instance shared across the app
Tier 2: State Providers (ChangeNotifier)
Providers manage reactive state:
// lib/main.dart:88-260
ChangeNotifierProvider<AuthProvider>.value(value: authProvider),
ChangeNotifierProxyProvider<AuthProvider, InventoryItemProvider>(...),
ChangeNotifierProxyProvider<AuthProvider, ThemeProvider>(...),
// ... more providers
Key characteristics:
- Extend
ChangeNotifier
- Call
notifyListeners() to trigger UI rebuilds
- Can depend on other providers (via ProxyProvider)
Provider Types
1. ChangeNotifierProvider
Used for independent state that doesn’t depend on other providers:
// lib/main.dart:88
ChangeNotifierProvider<AuthProvider>.value(value: authProvider)
Example: AuthProvider (lib/providers/auth_provider.dart)
class AuthProvider with ChangeNotifier {
UserData? _user;
String? _token;
bool _isLoading = true;
bool get isAuthenticated => _user != null && _token != null;
Future<LoginResponse> login(String username, String password) async {
_isLoading = true;
notifyListeners(); // UI shows loading state
try {
final response = await _apiService.login(username, password);
if (response.success && response.token != null) {
_token = response.token;
_user = response.user;
}
return response;
} finally {
_isLoading = false;
notifyListeners(); // UI updates with result
}
}
}
2. ChangeNotifierProxyProvider
Used when a provider depends on another provider’s state:
// lib/main.dart:171-185
ChangeNotifierProxyProvider<AuthProvider, InventoryItemProvider>(
create: (c) => InventoryItemProvider(
c.read<InventoryItemService>(),
c.read<AssetPrintService>(),
),
update: (context, auth, prev) {
// Reactive logic: when auth state changes, this runs
if (!auth.isLoading && auth.isAuthenticated && auth.token != null) {
if (prev != null && !prev.isLoading) {
Future.microtask(() => prev.loadAllItemsGlobal());
}
}
return prev!;
},
)
Pattern explanation:
create: Instantiates the provider once
update: Called whenever AuthProvider changes
auth: Current state of the dependency (AuthProvider)
prev: The existing instance of InventoryItemProvider
- Returns
prev! to maintain the same instance (preserves cache)
Common Provider Patterns
Pattern 1: Loading State
All providers follow this pattern for async operations:
class ExampleProvider with ChangeNotifier {
bool _isLoading = false;
bool get isLoading => _isLoading;
Future<void> loadData() async {
_isLoading = true;
notifyListeners(); // Show loading indicator
try {
// Fetch data
} catch (e) {
// Handle error
rethrow; // Let UI show error message
} finally {
_isLoading = false;
notifyListeners(); // Hide loading indicator
}
}
}
Example: lib/providers/inventory_item_provider.dart:292-346
Pattern 2: Data Caching
Providers cache data to minimize API calls:
// lib/providers/inventory_item_provider.dart:64-68
final Map<String, InventoryResponse> _itemsCache = {};
Future<void> loadInventoryItems({
required int containerId,
required int assetTypeId,
bool forceReload = false,
}) async {
final key = _getCacheKey(containerId, assetTypeId);
// Return cached data if available
if (_itemsCache.containsKey(key) && !forceReload) {
_recalculateTotalsAndNotify();
return;
}
// Otherwise fetch from API
_isLoading = true;
notifyListeners();
try {
final response = await _itemService.fetchInventoryItems(...);
_itemsCache[key] = response;
} finally {
_isLoading = false;
_recalculateTotalsAndNotify();
}
}
Pattern 3: Computed Getters
Providers expose processed data through getters:
// lib/providers/inventory_item_provider.dart:181-216
List<InventoryItem> get inventoryItems {
final cId = _currentContainerId;
final atId = _currentAssetTypeId;
final key = _getCacheKey(cId, atId);
final response = _itemsCache[key];
if (response == null) return [];
// Filter, sort, paginate
Iterable<InventoryItem> processedItems = _applyFilters(response.items);
List<InventoryItem> sortedList = processedItems.toList();
_applySort(sortedList);
// Pagination
final startIndex = (_currentPage - 1) * _itemsPerPage;
final endIndex = startIndex + _itemsPerPage;
return sortedList.sublist(startIndex, endIndex);
}
UI usage:
// Widgets automatically rebuild when inventoryItems changes
final items = context.watch<InventoryItemProvider>().inventoryItems;
Pattern 4: Auth-Dependent Initialization
Many providers auto-load data when the user logs in:
// lib/main.dart:154-168
ChangeNotifierProxyProvider<AuthProvider, DashboardProvider>(
create: (context) => DashboardProvider(context.read<DashboardService>()),
update: (context, auth, previous) {
if (!auth.isLoading && auth.isAuthenticated) {
// Only load if we haven't loaded yet
if (previous != null && previous.stats == null && !previous.isLoading) {
Future.microtask(() => previous.fetchStats());
}
}
return previous!;
},
)
Key points:
- Checks
!auth.isLoading to ensure auth is ready
- Checks
auth.isAuthenticated to ensure user is logged in
- Uses
Future.microtask() to avoid calling async code during build
- Prevents redundant loads with
previous.stats == null
Example Providers
AuthProvider
File: lib/providers/auth_provider.dart
Responsibilities:
- Manage authentication state (user, token, loading)
- Login/logout operations
- Profile updates
- GitHub OAuth integration
- Password management
Key methods:
login() - Authenticate user
logout() - Clear session
updateProfile() - Update user data
checkAuthStatus() - Restore session on app start
State:
UserData? _user;
String? _token;
bool _isLoading = true;
InventoryItemProvider
File: lib/providers/inventory_item_provider.dart
Responsibilities:
- Load and cache inventory items
- Filter, sort, paginate items (client-side)
- CRUD operations (create, update, delete, clone)
- Manage current container/asset type context
- Track price history and market value
Key methods:
loadInventoryItems() - Fetch items for a specific asset type
loadAllItemsGlobal() - Load all items across containers
createInventoryItem() - Add new item
updateAssetWithFiles() - Update item with file uploads
deleteInventoryItem() - Remove item
setFilter() - Apply search/filter
sortInventoryItems() - Sort by column
goToPage() - Navigate pagination
State:
Map<String, InventoryResponse> _itemsCache = {}; // Cached data
Map<String, String> _filters = {}; // Active filters
String? _globalSearchTerm; // Search query
int _currentPage = 1; // Pagination
int _itemsPerPage = 10;
bool _isLoading = false;
Computed getter:
List<InventoryItem> get inventoryItems {}
ContainerProvider
File: lib/providers/container_provider.dart
Responsibilities:
- Load container hierarchy
- Manage asset types within containers
- Load data lists (dropdowns)
- CRUD for containers, asset types, locations
Dependency injection:
// lib/main.dart:219-231
ChangeNotifierProxyProvider<AuthProvider, ContainerProvider>(
create: (c) => ContainerProvider(
c.read<ContainerService>(),
c.read<AssetTypeService>(),
c.read<LocationService>(),
),
update: (context, auth, prev) {
if (auth.isAuthenticated && auth.token != null && !auth.isLoading) {
Future.microtask(() => prev?.loadContainers());
}
return prev!;
},
)
PluginProvider
File: lib/providers/plugin_provider.dart
Responsibilities:
- Manage installed plugins
- Browse community plugins (GitHub + database)
- Install/uninstall plugins
- Activate/deactivate plugins
- Process plugin UI (STAC) with user context
Key methods:
refresh() - Reload plugin lists
install() - Install from marketplace
uninstall() - Remove plugin
togglePluginStatus() - Enable/disable
getProcessedUi() - Inject user data into plugin UI (e.g., {{userName}})
State:
List<StorePlugin> _installed = [];
List<StorePlugin> _community = [];
bool _isLoading = false;
UserData? _currentUser; // For UI template processing
ThemeProvider
File: lib/providers/theme_provider.dart
Responsibilities:
- Manage theme customization (colors, brightness)
- Sync with user profile theme config
- Persist theme changes to backend
Initialization:
// lib/main.dart:188-203
ChangeNotifierProxyProvider<AuthProvider, ThemeProvider>(
create: (c) => ThemeProvider(c.read<ThemeService>()),
update: (context, auth, prev) {
if (auth.isAuthenticated && auth.user?.themeConfig != null && !prev!.isInitialized) {
final config = auth.user!.themeConfig!;
prev.setInitializing();
prev.initializeThemeFromConfig(
config.theme.primaryColor,
config.theme.brightness,
);
}
return prev!;
},
)
PreferencesProvider
File: lib/providers/preferences_provider.dart
Responsibilities:
- Manage user preferences (language, currency, notifications)
- Load preferences on login
- Persist preference changes
Usage in app:
// lib/main.dart:292-304
final preferencesProvider = context.watch<PreferencesProvider>();
MaterialApp.router(
locale: preferencesProvider.locale,
// ...
)
Accessing Providers in UI
watch vs read vs select
// 1. watch - Rebuilds when provider changes
final items = context.watch<InventoryItemProvider>().inventoryItems;
// 2. read - One-time access (no rebuild)
context.read<InventoryItemProvider>().createInventoryItem(item);
// 3. select - Rebuild only when specific property changes
final isLoading = context.select<InventoryItemProvider, bool>(
(provider) => provider.isLoading,
);
For granular rebuilds:
Consumer<InventoryItemProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return CircularProgressIndicator();
}
return ListView(children: provider.inventoryItems.map(...));
},
)
For optimal performance (rebuild only on specific changes):
Selector<InventoryItemProvider, List<InventoryItem>>(
selector: (context, provider) => provider.inventoryItems,
builder: (context, items, child) {
return ListView(children: items.map(...));
},
)
State Update Flow
Typical flow for updating state:
1. User Action (Button Press)
↓
2. Widget calls provider method
context.read<ExampleProvider>().updateData(...)
↓
3. Provider updates internal state
_data = newData;
↓
4. Provider calls service
await _service.updateOnServer(newData);
↓
5. Provider notifies listeners
notifyListeners();
↓
6. Widgets rebuild
context.watch<ExampleProvider>().data rebuilds UI
Example: Updating an inventory item (lib/providers/inventory_item_provider.dart:403-432)
Future<InventoryItem> updateAssetWithFiles(
InventoryItem updatedItem, {
FileData filesToUpload = const [],
List<int> imageIdsToDelete = const [],
}) async {
_isLoading = true;
notifyListeners(); // Step 3: Show loading
try {
// Step 4: API call
final result = await _itemService.updateInventoryItem(
updatedItem,
filesToUpload: filesToUpload,
imageIdsToDelete: imageIdsToDelete,
);
// Step 5: Refresh cache
await loadInventoryItems(
containerId: updatedItem.containerId,
assetTypeId: updatedItem.assetTypeId,
forceReload: true,
);
return result; // Return fresh data to UI
} catch (e) {
_isLoading = false;
notifyListeners();
rethrow; // Let UI handle error
}
}
Best Practices
1. Single Responsibility
Each provider manages one domain:
- ✅
AuthProvider handles auth, InventoryItemProvider handles items
- ❌ Don’t mix concerns (e.g., auth logic in InventoryItemProvider)
2. Dispose Properly
Always dispose of resources:
// lib/providers/inventory_item_provider.dart:76-84
bool _isDisposed = false;
@override
void dispose() {
_isDisposed = true;
super.dispose();
}
@override
void notifyListeners() {
if (!_isDisposed) super.notifyListeners();
}
3. Avoid Unnecessary Rebuilds
Use context.select() or Selector for expensive widgets:
// Only rebuilds when isLoading changes, not on every provider change
final isLoading = context.select<ExampleProvider, bool>(
(p) => p.isLoading,
);
4. Error Handling
Rethrow errors for UI to display:
try {
await _service.riskyOperation();
} catch (e) {
_isLoading = false;
notifyListeners();
rethrow; // UI can show SnackBar/Toast
}
5. Use Future.microtask for Initialization
Avoid calling async methods during build:
// ❌ BAD - Throws error
update: (context, auth, prev) {
prev.loadData(); // Can't call async during build
return prev;
}
// ✅ GOOD
update: (context, auth, prev) {
Future.microtask(() => prev.loadData());
return prev;
}
6. Optimize with Getters
Compute data in getters, not in methods:
// ✅ GOOD - Computed on access
List<Item> get filteredItems => _items.where((i) => i.active).toList();
// ❌ BAD - Manual call required
void filterItems() {
_filteredItems = _items.where((i) => i.active).toList();
notifyListeners();
}
Testing Providers
Providers are easy to test in isolation:
test('AuthProvider login success', () async {
final mockApiService = MockApiService();
final provider = AuthProvider(apiService: mockApiService);
when(mockApiService.login(any, any)).thenAnswer(
(_) async => LoginResponse(success: true, token: 'abc123'),
);
final result = await provider.login('user', 'pass');
expect(result.success, true);
expect(provider.token, 'abc123');
expect(provider.isAuthenticated, true);
});
Next Steps