What is Maximo and how to use it with Flutter?
Answer
What is IBM Maximo?
IBM Maximo (Maximo Asset Management) is an enterprise-level Asset Management System used by organizations to manage physical assets, maintenance operations, inventory, and facilities throughout their lifecycle.
Key Features
- Asset Management: Track and manage physical assets (equipment, machinery, buildings)
- Work Order Management: Create, assign, and track maintenance tasks
- Inventory Management: Manage spare parts, tools, and materials
- Preventive Maintenance: Schedule and automate maintenance activities
- Mobile Workforce: Field technician management and dispatch
- Reporting & Analytics: Performance metrics and dashboards
Common Industries
- Manufacturing
- Oil & Gas
- Utilities (Power, Water)
- Transportation & Logistics
- Healthcare
- Facilities Management
- Government
Why Flutter with Maximo?
Use Cases
| Use Case | Description |
|---|---|
| Field Service App | Technicians access work orders on mobile devices |
| Asset Inspection | Scan QR codes, capture photos, record asset data |
| Inventory Management | Check stock levels, request parts, receive shipments |
| Work Order Creation | Create and submit work requests from the field |
| Offline Support | Work in areas without connectivity, sync later |
| Real-time Updates | Push notifications for urgent work orders |
Benefits
✅ Cross-platform: Single codebase for iOS and Android
✅ Native performance: Fast, responsive mobile UI
✅ Offline-first: SQLite local storage with sync
✅ Rich UI: Custom widgets for complex asset data
✅ Camera integration: Capture photos, scan barcodes
✅ GPS tracking: Location-based asset management
Maximo REST API Overview
API Architecture
Maximo provides RESTful APIs for integration with mobile and web applications.
Base URL Format:
texthttp://<maximo-server>/maximo/oslc/
Common Endpoints
| Endpoint | Purpose |
|---|---|
text | Work Orders |
text | Assets |
text | Service Requests |
text | Inventory |
text | Personnel |
text | Locations |
Authentication
Maximo typically uses:
- Basic Authentication (username/password)
- LDAP Authentication
- OAuth 2.0 (for modern deployments)
- API Keys (for service accounts)
Flutter Integration Architecture
Project Structure
textlib/ ├── main.dart ├── config/ │ └── maximo_config.dart # API configuration ├── models/ │ ├── work_order.dart # Work Order model │ ├── asset.dart # Asset model │ └── service_request.dart # Service Request model ├── services/ │ ├── maximo_api_service.dart # API client │ ├── auth_service.dart # Authentication │ └── sync_service.dart # Offline sync ├── repositories/ │ ├── work_order_repository.dart # Business logic │ └── asset_repository.dart # Data management ├── providers/ │ └── maximo_provider.dart # State management (Riverpod/Provider) ├── screens/ │ ├── work_orders/ │ │ ├── work_order_list_screen.dart │ │ └── work_order_detail_screen.dart │ └── assets/ │ └── asset_detail_screen.dart └── widgets/ └── work_order_card.dart
Step-by-Step Implementation
Step 1: Add Dependencies
pubspec.yaml:
yamldependencies: flutter: sdk: flutter # HTTP Client dio: ^5.4.0 # State Management flutter_riverpod: ^2.4.0 # Local Storage sqflite: ^2.3.0 path: ^1.8.3 # Secure Storage (for credentials) flutter_secure_storage: ^9.0.0 # JSON Serialization json_annotation: ^4.8.1 # Connectivity Check connectivity_plus: ^5.0.2 # Image Picker image_picker: ^1.0.7 # QR Code Scanner mobile_scanner: ^3.5.5 dev_dependencies: build_runner: ^2.4.7 json_serializable: ^6.7.1
Step 2: Configure Maximo Connection
lib/config/maximo_config.dart:
dartclass MaximoConfig { // Maximo server configuration static const String baseUrl = 'http://your-maximo-server.com/maximo'; static const String oslcPath = '/oslc'; // API endpoints static const String workOrdersEndpoint = '/os/mxwo'; static const String assetsEndpoint = '/os/mxasset'; static const String serviceRequestsEndpoint = '/os/mxsr'; static const String inventoryEndpoint = '/os/mxinventory'; // Authentication static const String authType = 'basic'; // 'basic', 'oauth', 'apikey' // Timeouts static const Duration connectionTimeout = Duration(seconds: 30); static const Duration receiveTimeout = Duration(seconds: 30); // Pagination static const int pageSize = 50; // Full endpoint URLs static String get workOrdersUrl => '$baseUrl$oslcPath$workOrdersEndpoint'; static String get assetsUrl => '$baseUrl$oslcPath$assetsEndpoint'; }
Step 3: Create Data Models
lib/models/work_order.dart:
dartimport 'package:json_annotation/json_annotation.dart'; part 'work_order.g.dart'; () class WorkOrder { (name: 'wonum') final String workOrderNumber; (name: 'description') final String description; (name: 'status') final String status; (name: 'assetnum') final String? assetNumber; (name: 'location') final String? location; (name: 'worktype') final String? workType; (name: 'priority') final int? priority; (name: 'reportdate') final DateTime? reportDate; (name: 'targstartdate') final DateTime? targetStartDate; (name: 'targcompdate') final DateTime? targetCompletionDate; (name: 'lead') final String? assignedTo; WorkOrder({ required this.workOrderNumber, required this.description, required this.status, this.assetNumber, this.location, this.workType, this.priority, this.reportDate, this.targetStartDate, this.targetCompletionDate, this.assignedTo, }); factory WorkOrder.fromJson(Map<String, dynamic> json) => _$WorkOrderFromJson(json); Map<String, dynamic> toJson() => _$WorkOrderToJson(this); } () class WorkOrdersResponse { (name: 'member') final List<WorkOrder> workOrders; (name: 'responseInfo') final ResponseInfo? responseInfo; WorkOrdersResponse({ required this.workOrders, this.responseInfo, }); factory WorkOrdersResponse.fromJson(Map<String, dynamic> json) => _$WorkOrdersResponseFromJson(json); } () class ResponseInfo { final int totalCount; final int pageNum; ResponseInfo({required this.totalCount, required this.pageNum}); factory ResponseInfo.fromJson(Map<String, dynamic> json) => _$ResponseInfoFromJson(json); }
lib/models/asset.dart:
dartimport 'package:json_annotation/json_annotation.dart'; part 'asset.g.dart'; () class Asset { (name: 'assetnum') final String assetNumber; (name: 'description') final String description; (name: 'status') final String status; (name: 'location') final String? location; (name: 'serialnum') final String? serialNumber; (name: 'manufacturer') final String? manufacturer; (name: 'model') final String? model; Asset({ required this.assetNumber, required this.description, required this.status, this.location, this.serialNumber, this.manufacturer, this.model, }); factory Asset.fromJson(Map<String, dynamic> json) => _$AssetFromJson(json); Map<String, dynamic> toJson() => _$AssetToJson(this); }
Step 4: Authentication Service
lib/services/auth_service.dart:
dartimport 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'dart:convert'; class AuthService { final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); static const String _usernameKey = 'maximo_username'; static const String _passwordKey = 'maximo_password'; static const String _tokenKey = 'maximo_auth_token'; // Save credentials Future<void> saveCredentials(String username, String password) async { await _secureStorage.write(key: _usernameKey, value: username); await _secureStorage.write(key: _passwordKey, value: password); } // Get Basic Auth header Future<String?> getBasicAuthHeader() async { final username = await _secureStorage.read(key: _usernameKey); final password = await _secureStorage.read(key: _passwordKey); if (username == null || password == null) { return null; } final credentials = base64Encode(utf8.encode('$username:$password')); return 'Basic $credentials'; } // Check if authenticated Future<bool> isAuthenticated() async { final username = await _secureStorage.read(key: _usernameKey); final password = await _secureStorage.read(key: _passwordKey); return username != null && password != null; } // Logout Future<void> logout() async { await _secureStorage.delete(key: _usernameKey); await _secureStorage.delete(key: _passwordKey); await _secureStorage.delete(key: _tokenKey); } }
Step 5: Maximo API Service
lib/services/maximo_api_service.dart:
dartimport 'package:dio/dio.dart'; import '../config/maximo_config.dart'; import '../models/work_order.dart'; import '../models/asset.dart'; import 'auth_service.dart'; class MaximoApiService { final Dio _dio; final AuthService _authService; MaximoApiService(this._authService) : _dio = Dio(BaseOptions( baseUrl: MaximoConfig.baseUrl, connectTimeout: MaximoConfig.connectionTimeout, receiveTimeout: MaximoConfig.receiveTimeout, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, )) { _setupInterceptors(); } void _setupInterceptors() { _dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) async { // Add authentication header final authHeader = await _authService.getBasicAuthHeader(); if (authHeader != null) { options.headers['Authorization'] = authHeader; } // Add MAXAUTH header (required by Maximo) options.headers['maxauth'] = authHeader; return handler.next(options); }, onError: (error, handler) async { // Handle 401 unauthorized if (error.response?.statusCode == 401) { await _authService.logout(); // Navigate to login screen } return handler.next(error); }, )); } // Get Work Orders Future<WorkOrdersResponse> getWorkOrders({ String? status, String? assignedTo, int page = 1, }) async { try { final queryParams = <String, dynamic>{ 'oslc.select': 'wonum,description,status,assetnum,location,worktype,priority,reportdate,targstartdate,targcompdate,lead', 'oslc.pageSize': MaximoConfig.pageSize, 'pageno': page, }; // Add filters final List<String> filters = []; if (status != null) { filters.add('status="$status"'); } if (assignedTo != null) { filters.add('lead="$assignedTo"'); } if (filters.isNotEmpty) { queryParams['oslc.where'] = filters.join(' and '); } final response = await _dio.get( '${MaximoConfig.oslcPath}${MaximoConfig.workOrdersEndpoint}', queryParameters: queryParams, ); return WorkOrdersResponse.fromJson(response.data); } on DioException catch (e) { throw _handleError(e); } } // Get Work Order by Number Future<WorkOrder> getWorkOrder(String woNum) async { try { final response = await _dio.get( '${MaximoConfig.oslcPath}${MaximoConfig.workOrdersEndpoint}/$woNum', ); return WorkOrder.fromJson(response.data); } on DioException catch (e) { throw _handleError(e); } } // Create Work Order Future<WorkOrder> createWorkOrder(WorkOrder workOrder) async { try { final response = await _dio.post( '${MaximoConfig.oslcPath}${MaximoConfig.workOrdersEndpoint}', data: workOrder.toJson(), ); return WorkOrder.fromJson(response.data); } on DioException catch (e) { throw _handleError(e); } } // Update Work Order Future<WorkOrder> updateWorkOrder(String woNum, Map<String, dynamic> updates) async { try { final response = await _dio.patch( '${MaximoConfig.oslcPath}${MaximoConfig.workOrdersEndpoint}/$woNum', data: updates, ); return WorkOrder.fromJson(response.data); } on DioException catch (e) { throw _handleError(e); } } // Change Work Order Status Future<WorkOrder> changeWorkOrderStatus(String woNum, String newStatus) async { return updateWorkOrder(woNum, {'status': newStatus}); } // Get Assets Future<List<Asset>> getAssets({String? location, String? status}) async { try { final queryParams = <String, dynamic>{ 'oslc.select': 'assetnum,description,status,location,serialnum,manufacturer,model', }; final List<String> filters = []; if (location != null) { filters.add('location="$location"'); } if (status != null) { filters.add('status="$status"'); } if (filters.isNotEmpty) { queryParams['oslc.where'] = filters.join(' and '); } final response = await _dio.get( '${MaximoConfig.oslcPath}${MaximoConfig.assetsEndpoint}', queryParameters: queryParams, ); final List<dynamic> data = response.data['member']; return data.map((json) => Asset.fromJson(json)).toList(); } on DioException catch (e) { throw _handleError(e); } } // Upload Attachment Future<void> uploadAttachment({ required String woNum, required String filePath, required String fileName, }) async { try { final formData = FormData.fromMap({ 'file': await MultipartFile.fromFile(filePath, filename: fileName), }); await _dio.post( '${MaximoConfig.oslcPath}${MaximoConfig.workOrdersEndpoint}/$woNum/doclinks', data: formData, ); } on DioException catch (e) { throw _handleError(e); } } // Error handling Exception _handleError(DioException error) { if (error.type == DioExceptionType.connectionTimeout) { return Exception('Connection timeout. Please check your network.'); } else if (error.type == DioExceptionType.receiveTimeout) { return Exception('Server response timeout.'); } else if (error.response != null) { final statusCode = error.response!.statusCode; final message = error.response!.data['Error']?['message'] ?? 'Unknown error'; switch (statusCode) { case 400: return Exception('Bad Request: $message'); case 401: return Exception('Unauthorized. Please login again.'); case 403: return Exception('Forbidden: You don\'t have permission.'); case 404: return Exception('Not Found: $message'); case 500: return Exception('Server Error: $message'); default: return Exception('Error $statusCode: $message'); } } return Exception('Network error: ${error.message}'); } }
Step 6: Local Database (Offline Support)
lib/services/database_service.dart:
dartimport 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import '../models/work_order.dart'; class DatabaseService { static final DatabaseService instance = DatabaseService._internal(); static Database? _database; DatabaseService._internal(); Future<Database> get database async { if (_database != null) return _database!; _database = await _initDatabase(); return _database!; } Future<Database> _initDatabase() async { final dbPath = await getDatabasesPath(); final path = join(dbPath, 'maximo_offline.db'); return await openDatabase( path, version: 1, onCreate: (db, version) async { // Work Orders table await db.execute(''' CREATE TABLE work_orders ( wonum TEXT PRIMARY KEY, description TEXT, status TEXT, assetnum TEXT, location TEXT, worktype TEXT, priority INTEGER, reportdate TEXT, targstartdate TEXT, targcompdate TEXT, assigned_to TEXT, is_synced INTEGER DEFAULT 0, modified_at TEXT ) '''); // Pending changes table (for offline edits) await db.execute(''' CREATE TABLE pending_changes ( id INTEGER PRIMARY KEY AUTOINCREMENT, entity_type TEXT, entity_id TEXT, action TEXT, data TEXT, created_at TEXT ) '''); }, ); } // Save work orders locally Future<void> saveWorkOrders(List<WorkOrder> workOrders) async { final db = await database; final batch = db.batch(); for (final wo in workOrders) { batch.insert( 'work_orders', { 'wonum': wo.workOrderNumber, 'description': wo.description, 'status': wo.status, 'assetnum': wo.assetNumber, 'location': wo.location, 'worktype': wo.workType, 'priority': wo.priority, 'reportdate': wo.reportDate?.toIso8601String(), 'targstartdate': wo.targetStartDate?.toIso8601String(), 'targcompdate': wo.targetCompletionDate?.toIso8601String(), 'assigned_to': wo.assignedTo, 'is_synced': 1, 'modified_at': DateTime.now().toIso8601String(), }, conflictAlgorithm: ConflictAlgorithm.replace, ); } await batch.commit(); } // Get local work orders Future<List<WorkOrder>> getLocalWorkOrders({String? status}) async { final db = await database; final where = status != null ? 'status = ?' : null; final whereArgs = status != null ? [status] : null; final List<Map<String, dynamic>> maps = await db.query( 'work_orders', where: where, whereArgs: whereArgs, orderBy: 'reportdate DESC', ); return maps.map((map) => WorkOrder( workOrderNumber: map['wonum'], description: map['description'], status: map['status'], assetNumber: map['assetnum'], location: map['location'], workType: map['worktype'], priority: map['priority'], reportDate: map['reportdate'] != null ? DateTime.parse(map['reportdate']) : null, targetStartDate: map['targstartdate'] != null ? DateTime.parse(map['targstartdate']) : null, targetCompletionDate: map['targcompdate'] != null ? DateTime.parse(map['targcompdate']) : null, assignedTo: map['assigned_to'], )).toList(); } // Queue offline change Future<void> queueChange({ required String entityType, required String entityId, required String action, required Map<String, dynamic> data, }) async { final db = await database; await db.insert('pending_changes', { 'entity_type': entityType, 'entity_id': entityId, 'action': action, 'data': data.toString(), 'created_at': DateTime.now().toIso8601String(), }); } // Get pending changes Future<List<Map<String, dynamic>>> getPendingChanges() async { final db = await database; return await db.query('pending_changes', orderBy: 'created_at ASC'); } // Clear pending changes after sync Future<void> clearPendingChanges() async { final db = await database; await db.delete('pending_changes'); } }
Step 7: Sync Service (Offline/Online)
lib/services/sync_service.dart:
dartimport 'package:connectivity_plus/connectivity_plus.dart'; import 'maximo_api_service.dart'; import 'database_service.dart'; class SyncService { final MaximoApiService _apiService; final DatabaseService _dbService = DatabaseService.instance; final Connectivity _connectivity = Connectivity(); SyncService(this._apiService); // Check connectivity Future<bool> isOnline() async { final result = await _connectivity.checkConnectivity(); return result != ConnectivityResult.none; } // Sync work orders from server to local Future<void> syncWorkOrders() async { if (!await isOnline()) { throw Exception('No internet connection'); } try { // Fetch from server final response = await _apiService.getWorkOrders(); // Save to local database await _dbService.saveWorkOrders(response.workOrders); print('Synced ${response.workOrders.length} work orders'); } catch (e) { throw Exception('Sync failed: $e'); } } // Push pending changes to server Future<void> pushPendingChanges() async { if (!await isOnline()) { throw Exception('No internet connection'); } try { final pendingChanges = await _dbService.getPendingChanges(); for (final change in pendingChanges) { final entityType = change['entity_type']; final entityId = change['entity_id']; final action = change['action']; // final data = change['data']; // Parse JSON if (entityType == 'work_order') { if (action == 'update') { // await _apiService.updateWorkOrder(entityId, data); } else if (action == 'create') { // await _apiService.createWorkOrder(data); } } } // Clear pending changes after successful sync await _dbService.clearPendingChanges(); print('Pushed ${pendingChanges.length} pending changes'); } catch (e) { throw Exception('Push failed: $e'); } } // Full bidirectional sync Future<void> fullSync() async { await pushPendingChanges(); await syncWorkOrders(); } }
Step 8: State Management (Riverpod)
lib/providers/maximo_provider.dart:
dartimport 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/work_order.dart'; import '../services/maximo_api_service.dart'; import '../services/auth_service.dart'; import '../services/database_service.dart'; import '../services/sync_service.dart'; // Services final authServiceProvider = Provider((ref) => AuthService()); final apiServiceProvider = Provider((ref) { final authService = ref.watch(authServiceProvider); return MaximoApiService(authService); }); final syncServiceProvider = Provider((ref) { final apiService = ref.watch(apiServiceProvider); return SyncService(apiService); }); // Work Orders State final workOrdersProvider = FutureProvider<List<WorkOrder>>((ref) async { final dbService = DatabaseService.instance; final syncService = ref.watch(syncServiceProvider); // Try to fetch from server if (await syncService.isOnline()) { try { await syncService.syncWorkOrders(); } catch (e) { print('Failed to sync: $e'); } } // Return local data return await dbService.getLocalWorkOrders(); }); // Work Orders by Status final workOrdersByStatusProvider = FutureProvider.family<List<WorkOrder>, String>((ref, status) async { final dbService = DatabaseService.instance; return await dbService.getLocalWorkOrders(status: status); });
Step 9: UI - Work Order List Screen
lib/screens/work_orders/work_order_list_screen.dart:
dartimport 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../providers/maximo_provider.dart'; import '../../widgets/work_order_card.dart'; class WorkOrderListScreen extends ConsumerWidget { const WorkOrderListScreen({Key? key}) : super(key: key); Widget build(BuildContext context, WidgetRef ref) { final workOrdersAsync = ref.watch(workOrdersProvider); return Scaffold( appBar: AppBar( title: const Text('Work Orders'), actions: [ IconButton( icon: const Icon(Icons.sync), onPressed: () { ref.refresh(workOrdersProvider); }, ), ], ), body: workOrdersAsync.when( data: (workOrders) { if (workOrders.isEmpty) { return const Center( child: Text('No work orders found'), ); } return RefreshIndicator( onRefresh: () async { ref.refresh(workOrdersProvider); }, child: ListView.builder( itemCount: workOrders.length, itemBuilder: (context, index) { final workOrder = workOrders[index]; return WorkOrderCard(workOrder: workOrder); }, ), ); }, loading: () => const Center( child: CircularProgressIndicator(), ), error: (error, stack) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error, size: 48, color: Colors.red), const SizedBox(height: 16), Text('Error: $error'), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref.refresh(workOrdersProvider), child: const Text('Retry'), ), ], ), ), ), ); } }
lib/widgets/work_order_card.dart:
dartimport 'package:flutter/material.dart'; import '../models/work_order.dart'; class WorkOrderCard extends StatelessWidget { final WorkOrder workOrder; const WorkOrderCard({Key? key, required this.workOrder}) : super(key: key); Widget build(BuildContext context) { return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: InkWell( onTap: () { // Navigate to detail screen }, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( workOrder.workOrderNumber, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), _buildStatusChip(workOrder.status), ], ), const SizedBox(height: 8), Text( workOrder.description, style: Theme.of(context).textTheme.bodyMedium, maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 12), _buildInfoRow(Icons.location_on, workOrder.location ?? 'N/A'), if (workOrder.assetNumber != null) _buildInfoRow(Icons.business, workOrder.assetNumber!), if (workOrder.assignedTo != null) _buildInfoRow(Icons.person, workOrder.assignedTo!), ], ), ), ), ); } Widget _buildStatusChip(String status) { Color color; switch (status.toUpperCase()) { case 'APPR': color = Colors.blue; break; case 'INPRG': color = Colors.orange; break; case 'COMP': color = Colors.green; break; case 'CLOSE': color = Colors.grey; break; default: color = Colors.grey; } return Chip( label: Text( status, style: const TextStyle(color: Colors.white, fontSize: 12), ), backgroundColor: color, padding: const EdgeInsets.symmetric(horizontal: 8), ); } Widget _buildInfoRow(IconData icon, String text) { return Padding( padding: const EdgeInsets.only(top: 4), child: Row( children: [ Icon(icon, size: 16, color: Colors.grey[600]), const SizedBox(width: 8), Expanded( child: Text( text, style: TextStyle(color: Colors.grey[700], fontSize: 13), ), ), ], ), ); } }
Best Practices
1. Error Handling
dart// ✅ Good: Handle network errors gracefully try { final workOrders = await apiService.getWorkOrders(); // Process data } on DioException catch (e) { if (e.type == DioExceptionType.connectionTimeout) { showError('Connection timeout. Check your network.'); } else { showError('Failed to load work orders'); } } catch (e) { showError('Unexpected error: $e'); }
2. Offline-First Architecture
dart// ✅ Good: Always fetch from local first Future<List<WorkOrder>> getWorkOrders() async { // 1. Return local data immediately final localData = await dbService.getLocalWorkOrders(); // 2. Sync in background if online if (await isOnline()) { syncService.syncWorkOrders().catchError((e) { print('Background sync failed: $e'); }); } return localData; }
3. Authentication Token Refresh
dart// ✅ Good: Auto-refresh expired tokens class MaximoApiService { void _setupInterceptors() { _dio.interceptors.add(InterceptorsWrapper( onError: (error, handler) async { if (error.response?.statusCode == 401) { // Token expired - refresh and retry final newToken = await _authService.refreshToken(); if (newToken != null) { error.requestOptions.headers['Authorization'] = newToken; return handler.resolve(await _dio.fetch(error.requestOptions)); } } return handler.next(error); }, )); } }
4. Pagination
dart// ✅ Good: Implement pagination for large datasets class WorkOrderRepository { int _currentPage = 1; bool _hasMore = true; Future<void> loadMore() async { if (!_hasMore) return; final response = await apiService.getWorkOrders(page: _currentPage); if (response.workOrders.length < MaximoConfig.pageSize) { _hasMore = false; } else { _currentPage++; } } }
Common Challenges & Solutions
Challenge 1: CORS Issues (Web)
Problem: Cross-Origin Resource Sharing blocked
Solution: Configure Maximo server to allow your domain
xml<!-- Maximo server web.xml --> <filter> <filter-name>CorsFilter</filter-name> <filter-class>org.apache.catalina.filters.CorsFilter</filter-class> <init-param> <param-name>cors.allowed.origins</param-name> <param-value>https://your-flutter-app.com</param-value> </init-param> </filter>
Challenge 2: Large Payloads
Problem: Slow response times for large datasets
Solution: Use selective fields
dart// ✅ Only fetch required fields final queryParams = { 'oslc.select': 'wonum,description,status', // Only 3 fields 'oslc.pageSize': 20, // Smaller page size };
Challenge 3: Certificate Issues (HTTPS)
Problem: Self-signed certificates rejected
Solution: Custom certificate validation (development only)
dart// ⚠️ Development only - DO NOT use in production (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) { client.badCertificateCallback = (cert, host, port) => true; return client; };
Testing
Unit Tests
dartimport 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; void main() { group('MaximoApiService', () { test('getWorkOrders returns list of work orders', () async { final mockAuthService = MockAuthService(); final apiService = MaximoApiService(mockAuthService); when(mockAuthService.getBasicAuthHeader()) .thenAnswer((_) async => 'Basic dGVzdDp0ZXN0'); final workOrders = await apiService.getWorkOrders(); expect(workOrders, isA<WorkOrdersResponse>()); expect(workOrders.workOrders.length, greaterThan(0)); }); }); }
Resources
- IBM Maximo REST API Documentation
- Maximo OSLC Guide
- Flutter Dio Package
- Flutter Riverpod Documentation
- Sqflite Package
Summary
| Aspect | Details |
|---|---|
| What is Maximo? | IBM's Enterprise Asset Management system |
| Integration Method | REST API (OSLC protocol) |
| Authentication | Basic Auth / OAuth 2.0 |
| Key Packages | dio, riverpod, sqflite, flutter_secure_storage |
| Architecture | Offline-first with bidirectional sync |
| Use Cases | Field service, asset inspection, work orders |
Key Takeaways
Maximo is an Enterprise Asset Management system providing RESTful APIs for integration
Use Dio for HTTP client with interceptors for auth and error handling
Implement offline-first architecture with SQLite for field workers
Use Riverpod for state management and reactive UI updates
Sync service handles bidirectional data synchronization
Always handle authentication, errors, and connectivity gracefully