What is Maximo and how to use it with Flutter?

#maximo#ibm#enterprise#rest-api#asset-management#offline-sync#dio#sqflite#field-service#oslc

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 CaseDescription
Field Service AppTechnicians access work orders on mobile devices
Asset InspectionScan QR codes, capture photos, record asset data
Inventory ManagementCheck stock levels, request parts, receive shipments
Work Order CreationCreate and submit work requests from the field
Offline SupportWork in areas without connectivity, sync later
Real-time UpdatesPush 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:

text
http://<maximo-server>/maximo/oslc/

Common Endpoints

EndpointPurpose
text
/os/mxwo
Work Orders
text
/os/mxasset
Assets
text
/os/mxsr
Service Requests
text
/os/mxinventory
Inventory
text
/os/mxperson
Personnel
text
/os/mxlocation
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

text
lib/
├── 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:

yaml
dependencies:
  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:

dart
class 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:

dart
import '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:

dart
import '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:

dart
import '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:

dart
import '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:

dart
import '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:

dart
import '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:

dart
import '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:

dart
import '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:

dart
import '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

dart
import '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


Summary

AspectDetails
What is Maximo?IBM's Enterprise Asset Management system
Integration MethodREST API (OSLC protocol)
AuthenticationBasic Auth / OAuth 2.0
Key Packagesdio, riverpod, sqflite, flutter_secure_storage
ArchitectureOffline-first with bidirectional sync
Use CasesField 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