# Flutter App's Provider System Architecture in the Omi Codebase

> Explore the Flutter app's provider system architecture in the Omi codebase. Learn how a centralized MultiProvider manages state and dependencies efficiently using ChangeNotifierProxyProvider.

- Repository: [omi/omi](https://github.com/basedhardware/omi)
- Tags: architecture
- Published: 2026-02-26

---

**The Omi Flutter app uses the Provider package with a centralized MultiProvider in `app/lib/main.dart` that instantiates all state management objects at the root, utilizing ChangeNotifierProxyProvider variants to handle complex inter-provider dependencies.**

The Omi open-source project implements a robust state management layer using Flutter's Provider package. Understanding the Flutter app's provider system architecture is essential for developers contributing to the codebase or building similar applications. This architecture centralizes all providers in a single MultiProvider widget at the application root, establishing clear dependency chains through proxy providers while maintaining clean separation of concerns across authentication, capture, device management, and UI state.

## Root Provider Configuration in main.dart

All providers are instantiated in `app/lib/main.dart` within a single **MultiProvider** widget that wraps the MaterialApp. This centralized approach ensures every widget in the tree can access state through the BuildContext.

The configuration uses specific provider variants based on state requirements:

- **ListenableProvider**: For simple listenable objects that don't require `listen: false` patterns
- **ChangeNotifierProvider**: For standard mutable state with notification capabilities  
- **ChangeNotifierProxyProvider**: For providers depending on a single other provider
- **ChangeNotifierProxyProvider2/4**: For providers with multiple dependencies

## Provider Types and Their Roles

### ListenableProvider

Used for the **ConnectivityProvider**, this variant supplies objects that implement Listenable without requiring the overhead of ChangeNotifier's mutation patterns.

### ChangeNotifierProvider

The workhorse of the architecture, handling **AuthenticationProvider**, **ConversationProvider**, **AppProvider**, and others. These hold mutable state and call `notifyListeners()` when updates occur.

Some use lazy initialization disabled (`lazy: false`) for providers like **CalendarProvider** that must initialize immediately at startup. Others chain initialization methods:

```dart
ChangeNotifierProvider(
  create: (_) => DeveloperModeProvider()..initialize(),
),

```

### ChangeNotifierProxyProvider Variants

These handle complex dependency injection. The architecture uses **ChangeNotifierProxyProvider** when a provider needs access to another provider's state or methods.

## Dependency Hierarchy and Injection Patterns

The provider system establishes a strict dependency chain where providers must appear **after** their dependencies in the MultiProvider list. The hierarchy flows as follows:

1. **Base providers** (Connectivity, Authentication, Conversation, App, People, Usage)
2. **Dependent providers** (MessageProvider depends on AppProvider)
3. **Complex dependencies** (CaptureProvider depends on Conversation, Message, People, and Usage providers via ChangeNotifierProxyProvider4)
4. **Device layer** (DeviceProvider depends on CaptureProvider)
5. **UI layer** (OnboardingProvider, SpeechProfileProvider depend on DeviceProvider)

### Proxy Provider Implementation

The **DeviceProvider** demonstrates the proxy pattern:

```dart
class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption {
  late CaptureProvider _capture;

  void setProviders(CaptureProvider capture) {
    _capture = capture;
    _capture.addListener(_onCaptureChanged);
  }

  void _onCaptureChanged() {
    notifyListeners();
  }
}

```

The proxy provider in `main.dart` wires this dependency:

```dart
ChangeNotifierProxyProvider<CaptureProvider, DeviceProvider>(
  create: (_) => DeviceProvider(),
  update: (_, capture, device) {
    device?.setProviders(capture);
    return device!;
  },
),

```

## Consuming Providers in the UI

The architecture supports multiple consumption patterns throughout the widget tree:

### Direct Read Without Rebuild

For one-time actions or method calls:

```dart
void _startRecording(BuildContext context) async {
  final capture = Provider.of<CaptureProvider>(context, listen: false);
  await capture.streamSystemAudioRecording();
}

```

### Reactive Rebuilds

For UI that updates when state changes:

```dart
class RecordButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final isRecording = context.watch<CaptureProvider>().recordingState == RecordingState.record;
    return IconButton(
      icon: Icon(isRecording ? Icons.stop : Icons.mic),
      onPressed: () => _handleRecordButtonPress(context, Provider.of<CaptureProvider>(context, listen: false)),
    );
  }
}

```

### Scoped Consumer Widgets

For granular rebuild control:

```dart
Consumer<LocaleProvider>(
  builder: (ctx, localeProvider, _) => Text(localeProvider.locale?.toString() ?? ''),
);

```

## Key Files in the Provider Architecture

| File | Role |
|------|------|
| `app/lib/main.dart` | Root MultiProvider declaration and wiring |
| `app/lib/providers/capture_provider.dart` | Core audio capture logic with multiple dependencies |
| `app/lib/providers/device_provider.dart` | Device connection management via proxy pattern |
| `app/lib/providers/authentication_provider.dart` | Firebase auth state |
| `app/lib/providers/conversation_provider.dart` | Conversation data management |
| `app/lib/providers/conversation_detail_provider.dart` | Detail view with AppProvider and ConversationProvider dependencies |

## Summary

- The Omi Flutter app centralizes all state management in a single MultiProvider at the root of the widget tree in `app/lib/main.dart`
- Provider types are selected based on state complexity: ListenableProvider for simple listeners, ChangeNotifierProvider for mutable state, and ChangeNotifierProxyProvider variants for dependency injection
- The architecture enforces a strict dependency hierarchy where providers must be declared after their dependencies, enabling complex multi-provider chains like CaptureProvider depending on four separate providers
- UI components consume state through `Provider.of` with `listen: false` for actions, `context.watch` for reactive rebuilds, or `Consumer` widgets for scoped updates

## Frequently Asked Questions

### What is the root entry point for all providers in the Omi Flutter app?

All providers are instantiated in the MultiProvider widget located in `app/lib/main.dart`. This widget wraps the MaterialApp and serves as the single source of truth for dependency injection throughout the application.

### How does the Omi app handle providers that depend on multiple other providers?

The architecture uses **ChangeNotifierProxyProvider4** (and similar variants) to inject multiple dependencies. For example, CaptureProvider receives instances of ConversationProvider, MessageProvider, PeopleProvider, and UsageProvider through a ChangeNotifierProxyProvider4 declaration in main.dart.

### Why does the order of providers in the MultiProvider list matter?

Provider order determines dependency availability. A ChangeNotifierProxyProvider cannot locate its source dependencies if they are declared later in the list. The Omi app places base providers (Authentication, Conversation, App) before dependent providers (Message, Capture, Device) to ensure the proxy update callbacks receive valid instances.

### What is the difference between ListenableProvider and ChangeNotifierProvider in this architecture?

**ListenableProvider** is used for objects that implement Listenable but don't require the full ChangeNotifier API, such as ConnectivityProvider. **ChangeNotifierProvider** is used for stateful objects that mutate data and call `notifyListeners()`, such as AuthenticationProvider and CaptureProvider. The choice depends on whether the provider needs to manage complex mutable state or simply broadcast events.