Prodobit Flutter SDK
A modern, type-safe Flutter SDK for building manufacturing and business applications with the Prodobit framework.
🚀 Features
- Modern Architecture: Built with latest Flutter patterns and best practices
- Type Safety: Full TypeScript-equivalent type safety with Freezed models
- State Management: Riverpod for reactive state management
- Authentication: Secure JWT-based authentication with token refresh
- Networking: Robust HTTP client with error handling and retry logic
- Caching: Intelligent caching for better performance
- File Upload: Built-in file upload with progress tracking
- Multi-tenancy: Organization context management
- Error Handling: Comprehensive error handling with custom exceptions
📦 Installation
Add to your pubspec.yaml:
dependencies:
prodobit_flutter_sdk: ^0.1.0
flutter_riverpod: ^2.5.1
🛠 Setup
1. Initialize the Client
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:prodobit_flutter_sdk/prodobit_flutter_sdk.dart';
// Override the client provider with your configuration
final prodobitClientProvider = Provider<ProdobitClient>((ref) {
return ProdobitClient(
baseUrl: 'https://your-api.prodobit.com',
enableLogging: kDebugMode,
);
});
2. Wrap Your App
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
title: 'My Manufacturing App',
home: AuthWrapper(),
);
}
}
3. Initialize Authentication
class AuthWrapper extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authProvider);
return authState.when(
data: (state) => state.when(
authenticated: (user, org) => HomeScreen(),
unauthenticated: () => LoginScreen(),
loading: () => LoadingScreen(),
error: (message) => ErrorScreen(message: message),
initial: () => SplashScreen(),
),
loading: () => LoadingScreen(),
error: (error, stack) => ErrorScreen(message: error.toString()),
);
}
}
🔐 Authentication
OTP Email Authentication
Prodobit uses OTP (One-Time Password) email authentication. Users receive a verification code via email.
class LoginScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: LoginFlow(),
);
}
}
class LoginFlow extends ConsumerStatefulWidget {
@override
ConsumerState<LoginFlow> createState() => _LoginFlowState();
}
class _LoginFlowState extends ConsumerState<LoginFlow> {
final _emailController = TextEditingController();
final _otpController = TextEditingController();
bool _otpSent = false;
@override
Widget build(BuildContext context) {
final authNotifier = ref.read(authProvider.notifier);
return Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!_otpSent) ...[
// Email input step
TextField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email Address',
hintText: 'Enter your email',
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
try {
await authNotifier.sendOTP(_emailController.text);
setState(() {
_otpSent = true;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('OTP sent to your email')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to send OTP: $e')),
);
}
},
child: Text('Send OTP'),
),
] else ...[
// OTP verification step
Text(
'Enter the verification code sent to ${_emailController.text}',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
SizedBox(height: 20),
TextField(
controller: _otpController,
decoration: InputDecoration(
labelText: 'Verification Code',
hintText: 'Enter 6-digit code',
),
keyboardType: TextInputType.number,
maxLength: 6,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
try {
await authNotifier.verifyOTP(
email: _emailController.text,
otp: _otpController.text,
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Invalid OTP: $e')),
);
}
},
child: Text('Verify & Login'),
),
SizedBox(height: 10),
TextButton(
onPressed: () async {
try {
await authNotifier.sendOTP(_emailController.text);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('New OTP sent')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to resend OTP: $e')),
);
}
},
child: Text('Resend OTP'),
),
TextButton(
onPressed: () {
setState(() {
_otpSent = false;
_otpController.clear();
});
},
child: Text('Change Email'),
),
],
],
),
);
}
}
Token Management
After successful OTP verification, the SDK automatically handles JWT tokens:
// Tokens are automatically stored securely and refreshed when needed
// No manual token management required
Logout
ElevatedButton(
onPressed: () async {
final authNotifier = ref.read(authProvider.notifier);
await authNotifier.logout();
},
child: Text('Logout'),
),
📦 Inventory Management
List Items
class ItemsScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final itemsAsync = ref.watch(itemsProvider());
return Scaffold(
appBar: AppBar(title: Text('Items')),
body: itemsAsync.when(
data: (paginatedItems) => ListView.builder(
itemCount: paginatedItems.data.length,
itemBuilder: (context, index) {
final item = paginatedItems.data[index];
return ListTile(
title: Text(item.displayName),
subtitle: Text(item.description ?? ''),
leading: item.hasImage
? Image.network(item.imageUrl!)
: Icon(Icons.inventory),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ItemDetailScreen(itemId: item.id),
),
),
);
},
),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorWidget(error),
),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => CreateItemScreen()),
),
child: Icon(Icons.add),
),
);
}
}
Create Item
class CreateItemScreen extends ConsumerWidget {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _codeController = TextEditingController();
final _descriptionController = TextEditingController();
@override
Widget build(BuildContext context, WidgetRef ref) {
final operations = ref.read(inventoryOperationsProvider.notifier);
return Scaffold(
appBar: AppBar(title: Text('Create Item')),
body: Form(
key: _formKey,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: InputDecoration(labelText: 'Name'),
validator: (value) => Validators.required(value, fieldName: 'Name'),
),
TextFormField(
controller: _codeController,
decoration: InputDecoration(labelText: 'Code'),
validator: Validators.itemCode,
),
TextFormField(
controller: _descriptionController,
decoration: InputDecoration(labelText: 'Description'),
maxLines: 3,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
try {
await operations.createItem(
ItemRequest(
name: _nameController.text,
code: _codeController.text,
description: _descriptionController.text.isEmpty
? null : _descriptionController.text,
type: ItemTypes.product,
unit: Defaults.unit,
),
);
Navigator.pop(context);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${e.toString()}')),
);
}
}
},
child: Text('Create Item'),
),
],
),
),
),
);
}
}
Stock Management
class StockScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final lowStockAsync = ref.watch(lowStockItemsProvider);
return Scaffold(
appBar: AppBar(title: Text('Stock Management')),
body: Column(
children: [
// Low Stock Alert
Container(
padding: EdgeInsets.all(16),
color: Colors.orange.shade100,
child: lowStockAsync.when(
data: (lowStockItems) => lowStockItems.isEmpty
? Text('All items are well stocked')
: Text('${lowStockItems.length} items are low on stock'),
loading: () => Text('Checking stock levels...'),
error: (_, __) => Text('Error checking stock levels'),
),
),
// Stock Actions
Expanded(
child: GridView.count(
crossAxisCount: 2,
padding: EdgeInsets.all(16),
children: [
_buildActionCard(
icon: Icons.add_box,
title: 'Stock In',
onTap: () => _showStockAdjustment(context, ref, isIncrease: true),
),
_buildActionCard(
icon: Icons.remove_circle,
title: 'Stock Out',
onTap: () => _showStockAdjustment(context, ref, isIncrease: false),
),
_buildActionCard(
icon: Icons.swap_horiz,
title: 'Transfer',
onTap: () => _showStockTransfer(context, ref),
),
_buildActionCard(
icon: Icons.history,
title: 'Movements',
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StockMovementsScreen(),
),
),
),
],
),
),
],
),
);
}
Widget _buildActionCard({
required IconData icon,
required String title,
required VoidCallback onTap,
}) {
return Card(
child: InkWell(
onTap: onTap,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 48),
SizedBox(height: 8),
Text(title, style: TextStyle(fontWeight: FontWeight.bold)),
],
),
),
);
}
void _showStockAdjustment(
BuildContext context,
WidgetRef ref, {
required bool isIncrease,
}) {
// Implementation for stock adjustment dialog
}
void _showStockTransfer(BuildContext context, WidgetRef ref) {
// Implementation for stock transfer dialog
}
}
🔍 Advanced Usage
Custom Error Handling
class CustomErrorHandler {
static void handle(Object error, WidgetRef ref) {
if (error is AuthException) {
// Handle authentication errors
ref.read(authProvider.notifier).logout();
// Navigate to login
} else if (error is ApiException) {
// Handle API errors
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(error.message)),
);
} else {
// Handle other errors
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('An unexpected error occurred')),
);
}
}
}
File Upload
class FileUploadWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
try {
final client = ref.read(prodobitClientProvider);
final uploadResponse = await client.file.uploadFile(
image.path,
onProgress: (progress) {
// Update progress indicator
print('Upload progress: ${(progress * 100).toInt()}%');
},
);
print('File uploaded: ${uploadResponse.url}');
} catch (e) {
print('Upload failed: $e');
}
}
},
child: Text('Upload Image'),
);
}
}
Search & Filtering
class ItemSearchScreen extends ConsumerStatefulWidget {
@override
ConsumerState<ItemSearchScreen> createState() => _ItemSearchScreenState();
}
class _ItemSearchScreenState extends ConsumerState<ItemSearchScreen> {
final _searchController = TextEditingController();
String _searchQuery = '';
@override
Widget build(BuildContext context) {
final itemsAsync = ref.watch(itemsProvider(
query: QueryParams(
search: _searchQuery,
limit: 50,
),
));
return Scaffold(
appBar: AppBar(
title: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search items...',
border: InputBorder.none,
),
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
),
),
body: itemsAsync.when(
data: (paginatedItems) => ListView.builder(
itemCount: paginatedItems.data.length,
itemBuilder: (context, index) {
final item = paginatedItems.data[index];
return ListTile(
title: Text(item.displayName),
subtitle: Text(item.description ?? ''),
);
},
),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorWidget(error),
),
);
}
}
📝 Best Practices
1. Error Handling
Always wrap async operations in try-catch blocks and handle different exception types appropriately.
2. State Management
Use Riverpod providers consistently and invalidate them when data changes.
3. Type Safety
Leverage Freezed models for type safety and use the built-in validators for form validation.
4. Performance
Use pagination for large datasets and implement proper loading states.
5. Security
Never store sensitive data in plain text. The SDK handles token storage securely.
🏗 Architecture
The SDK follows clean architecture principles:
- Core: API client, configuration, and base types
- Models: Freezed data models with JSON serialization
- Services: Business logic and API interactions
- Providers: Riverpod state management
- Utils: Helpers, extensions, and utilities
🤝 Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
🆘 Support
For support, please visit Prodobit Documentation or open an issue on GitHub.
Built with ❤️ by the Prodobit team
Libraries
- core/api_client
- core/auth_interceptor
- core/cache_setup
- core/prodobit_client
- core/prodobit_config
- exceptions/api_exception
- exceptions/auth_exception
- exceptions/exceptions
- exceptions/prodobit_exception
- exceptions/validation_exception
- models/asset/asset_models
- models/asset_issue/asset_issue_models
- models/auth/auth_models
- models/calibration/calibration_models
- models/common/common_models
- models/employee/employee_models
- models/inventory/inventory_models
- models/inventory/lot_management_models
- models/inventory/physical_inventory_models
- models/inventory/stock_adjustment_models
- models/inventory/stock_reservation_models
- models/maintenance/maintenance_models
- models/manufacturing/bom_models
- models/manufacturing/eco_models
- models/manufacturing/manufacturing_models
- models/models
- models/party/contact_models
- models/party/enhanced_party_models
- models/party/party_models
- models/purchase/goods_receipt_models
- models/purchase/purchase_order_models
- models/purchase/purchase_requisition_models
- models/purchase/rfq_models
- models/purchase/supplier_quotation_models
- models/response/response_models
- models/sales/sales_models
- models/sales/sales_order_models
- models/task/task_models
- models/tenant/tenant_models
- models/user/user_models
- prodobit_flutter_sdk
- providers/auth_provider
- providers/client_provider
- providers/inventory_provider
- providers/providers
- providers/user_provider
- services/asset_issue_service
- services/asset_service
- services/auth_service
- services/calibration_service
- services/employee_service
- services/file_service
- services/inventory_service
- services/location_service
- services/maintenance_service
- services/manufacturing_service
- services/party_service
- services/purchase_service
- services/sales_service
- services/services
- services/task_service
- services/tenant_service
- services/user_service
- utils/constants
- utils/extensions
- utils/utils
- utils/validators