In the enterprise world, multi-company support isn't just a feature; it's a requirement. Odoo handles this natively in its web interface, allowing users to seamlessly switch contexts and view data across different business units. However, when building custom mobile apps with Flutter, replicating this behavior requires a thoughtful architecture.
If you look at comprehensive Odoo mobile solutions like the Mobo app suite—which splits complex Odoo modules into role-specific, user-friendly mobile applications—managing this context flawlessly is critical.
In this guide, we'll explore how to implement a production-grade multi-company system in Flutter that mimics the native Odoo experience, utilizing the same architectural principles that power apps like Mobo.
The Challenge: Context is Everything
Odoo uses a concept called the Current Context to determine which records a user can access. For multi-company environments, the most critical key in this context is allowed_company_ids.
If your API calls don't explicitly include this list, Odoo defaults to the user's primary company. This means a user could have access to "Company A" and "Company B", but if they select "Company B" in your app and you don't tell the backend, they'll still see data for "Company A".
To fix this, we need a Context-First Architecture where the active company state is:
- Stored globally in the app session.
- Injected automatically into every network request.
- Persisted locally so the user's choice survives app restarts.
Step 1: Modeling the Session
First, in the mobo app, we need to extend our session model to include the user’s selected company. A typical class might look like this:
class AppSession {
final OdooSession odooSession;
final int? selectedCompanyId; // The "Main" active company
final List<int> allowedCompanyIds; // All active companies (Main + secondary)
const AppSession({
required this.odooSession,
this.selectedCompanyId,
this.allowedCompanyIds = const [],
// ... other session fields
});
// Example method to save preferences locally
Future<void> saveToPreferences() async {
final prefs = await SharedPreferences.getInstance();
if (selectedCompanyId != null) {
await prefs.setInt('selected_company_id', selectedCompanyId!);
}
await prefs.setStringList(
'allowed_company_ids',
allowedCompanyIds.map((e) => e.toString()).toList(),
);
}
}This ensures that the "company context" is always coupled with the "user authentication" state.
Step 2: The Network Interceptor
The most robust way to handle multi-company support is to make it invisible to your feature code. You shouldn't have to manually pass company IDs in every single repository method.
Instead, create a centralized Request Manager that intercepts every call and injects the context.
class OdooRequestManager {
// ... client initialization ...
/// A wrapper for Odoo's 'call_kw' method that automatically injects context
Future<dynamic> callKw({
required String model,
required String method,
required List args,
Map<String, dynamic> kwargs = const {},
}) async {
final session = await _sessionService.getCurrentSession();
// 1. Get existing context or create new one
Map<String, dynamic> context = Map.from(kwargs['context'] ?? {});
// 2. Inject Allowed Companies
if (session != null && session.allowedCompanyIds.isNotEmpty) {
context['allowed_company_ids'] = session.allowedCompanyIds;
}
// 3. Update kwargs with the new context
final newKwargs = Map<String, dynamic>.from(kwargs);
newKwargs['context'] = context;
// 4. execute the call
return _client.callKw(
model, method, args, newKwargs
);
}
}With this in place, a developer writing a ProductRepository simply calls requestManager.callKw(...) without worrying about which company is active. The backend always receives the correct filter.
Step 3: Managing State with a Provider
You'll need a state management solution (like Provider, Riverpod, or Bloc) to handle the logic of switching companies. This provider acts as the brain, ensuring that invalid states (like selecting a company you don't have access to) are impossible.
class CompanyProvider extends ChangeNotifier {
List<Company> _availableCompanies = [];
int? _activeCompanyId;
Future<void> switchCompany(int newCompanyId) async {
// 1. Validate: Ensure user actually has access to this company
if (!_availableCompanies.any((c) => c.id == newCompanyId)) return;
// 2. Update Local State
_activeCompanyId = newCompanyId;
// 3. Update Session & Persist
await _sessionService.updateCompanyContext(
selectedId: newCompanyId,
// For simple use cases, usually the selected company is the only allowed one.
// For advanced cases, you might allow multiple selections (Multi-Company mode).
allowedIds: [newCompanyId],
);
// 4. Notify UI to rebuild
notifyListeners();
}
}Step 4: The UI Switcher
Finally, users need a way to switch contexts. A common pattern in tailored apps like Mobo CRM or Mobo POS is to place a dropdown or a bottom sheet in the app bar.
When designing the UI, remember that Odoo supports two modes:
- Single Selection: Switching the main active company.
- Multi-Selection: "Checking" additional companies to view their data simultaneously.
For a mobile app, a simple dropdown that switches the primary company is often sufficient for 90% of use cases.
// Simple Company Selector Widget
DropdownButton<int>(
value: provider.activeCompanyId,
items: provider.availableCompanies.map((company) {
return DropdownMenuItem(
value: company.id,
child: Text(company.name),
);
}).toList(),
onChanged: (newId) {
if (newId != null) {
provider.switchCompany(newId);
}
},
)
By baking multi-company support into your application's networking layer, you dramatically reduce complexity and potential bugs. Your UI developers can focus on building features, confident that the data users see is always scoped to the correct business unit.
To read more about How to Implement Offline Mode in Mobo with Odoo Data Sync, refer to our blog How to Implement Offline Mode in Mobo with Odoo Data Sync.