# How Riverpod State Management Integrates with FlClash's CoreController

> Learn how FlClash integrates Riverpod state management with its CoreController for clean UI separation and efficient proxy operations. Understand the reactive state layer in action.

- Repository: [chen08209/FlClash](https://github.com/chen08209/FlClash)
- Tags: how-to-guide
- Published: 2026-05-31

---

**FlClash uses Riverpod as a reactive state layer that delegates platform-specific proxy operations to a singleton CoreController, creating a clean separation between UI state management and the underlying Clash Meta engine.**

FlClash is a cross-platform proxy client built with Flutter that leverages **Riverpod state management** to maintain a responsive UI while offloading heavy lifting to a native Go core. The architecture centers on the **CoreController**, a singleton façade that abstracts platform-specific FFI calls and socket services. Understanding how these components interact is essential for contributors working with the `chen08209/FlClash` codebase.

## Architecture Overview

The codebase follows a strict separation of concerns between the imperative native engine and the reactive Flutter UI. The **CoreController** handles low-level proxy operations, while **Riverpod providers** manage application state and widget rebuilds. This pattern ensures the Flutter layer remains testable, platform-agnostic, and responsive to changes in the underlying proxy engine.

## CoreController as the Bridge to the Native Engine

Located in `lib/core/controller.dart`, the **CoreController** acts as a singleton façade that forwards all proxy-related calls to either the Android FFI library (`coreLib`) or the desktop socket service (`coreService`). It exposes async methods including `init()`, `getConfig()`, `changeProxy()`, and `getTraffic()`.

The controller maintains an internal completion state via `isCompleted`, which indicates whether the Go core has finished its startup sequence. This flag is critical for lifecycle management, as providers check it before attempting to restart or reconfigure the engine.

## Riverpod Providers for Reactive UI State

Riverpod providers annotated with `@riverpod` or `@Riverpod(keepAlive: true)` encapsulate UI-specific state such as traffic statistics, proxy groups, and selected profiles. These providers live in `lib/providers/action.dart` and `lib/providers/state.dart`.

**Action notifiers** serve as the coordination layer. Classes like `CommonAction`, `CoreAction`, and `SetupAction` invoke methods on the global `coreController` instance, await results, then update Riverpod state to trigger UI rebuilds. This keeps UI widgets free of business logic while maintaining reactive data flows.

## The Bidirectional Interaction Pattern

The communication between Riverpod and CoreController follows a predictable cycle: notifiers trigger CoreController operations, process the returned data, then persist results back into Riverpod state. This creates a unidirectional data flow from user actions through the core engine and back to the interface.

### Initializing the Core with CoreAction

The `CoreAction` notifier handles core initialization when the application starts. It first checks the CoreController's initialization status via `isInit` before invoking the expensive `init()` method.

```dart
class CoreAction extends _$CoreAction {
  @override
  void build() {}

  Future<void> initCore() async {
    final alreadyInit = await coreController.isInit;
    final version = ref.read(versionProvider);
    if (!alreadyInit) {
      // Initialise the Go core
      final result = await coreController.init(version);
      commonPrint.log('init result: $result');
    } else {
      // Core already running – just refresh groups
      await ref.read(proxiesActionProvider.notifier).updateGroups();
    }
  }
}

```

### Fetching Traffic Data via CommonAction

`CommonAction` coordinates periodic traffic updates by reading user preferences from `appSettingProvider`, fetching statistics via `coreController.getTraffic()`, and storing results in `trafficsProvider` and `totalTrafficProvider`.

```dart
class CommonAction extends _$CommonAction {
  @override
  void build() {}

  Future<void> updateTraffic() async {
    final onlyStats = ref.read(
      appSettingProvider.select((s) => s.onlyStatisticsProxy),
    );
    // Call the Core Controller
    final traffic = await coreController.getTraffic(onlyStats);
    // Store result in a Riverpod state notifier
    ref.read(trafficsProvider.notifier).addTraffic(traffic);
    // Also update total traffic state
    ref.read(totalTrafficProvider.notifier).value =
        await coreController.getTotalTraffic(onlyStats);
  }
}

```

### Managing Proxy Lifecycle with SetupAction

`SetupAction` orchestrates the start/stop sequence for the proxy service. The `updateStatus()` method checks `coreController.isCompleted` before attempting restarts, starts the Core's listener via `_handleStart()`, and manages the periodic timer that drives runtime UI updates.

```dart
class SetupAction extends _$SetupAction {
  Future<void> updateStatus(bool start, {bool isInit = false}) async {
    if (start) {
      if (!isInit) {
        final canStart = await ref
            .read(coreActionProvider.notifier)
            .tryStartCore(true);
        if (canStart) return;
        await _handleStart();               // starts listener & timer
        applyProfileDebounce(force: true);   // applies current profile
      } else {
        // start after app restart
        await _handleStart();
        await applyProfile(force: true);
      }
    } else {
      await handleStop();                    // stops listener, clears traffic
      coreController.resetTraffic();
    }
  }
}

```

### Deriving State Directly from the Core

Some providers read directly from the CoreController to derive fresh data without maintaining persistent state. The `clashConfig` provider in `lib/providers/state.dart` fetches raw configuration JSON via `getConfig()` and transforms it into typed `ClashConfig` objects.

```dart
@riverpod
Future<ClashConfig> clashConfig(Ref ref, int profileId) async {
  // Direct Core Controller call
  final configMap = await coreController.getConfig(profileId);
  return ClashConfig.fromJson(configMap);
}

```

## Key Files and Implementation Details

Understanding the file structure helps navigate the interaction between Riverpod and the CoreController:

- **`lib/core/controller.dart`** – Contains the singleton `CoreController` class that abstracts platform-specific implementations.
- **`lib/providers/action.dart`** – Houses Riverpod notifiers (`CommonAction`, `SetupAction`, `CoreAction`) that orchestrate core operations.
- **`lib/providers/state.dart`** – Defines reactive providers including `clashConfig` that fetch data directly from `coreController`.
- **`lib/providers/generated/*.g.dart`** – Contains Riverpod boilerplate generated by code generation (not hand-written).
- **`lib/main.dart`** – Application entry point that creates the `ProviderScope` and supplies the global `coreController` instance.
- **`lib/common/common.dart`** – Utility helpers shared between Riverpod actions and the CoreController.

## Summary

- The **CoreController** in `lib/core/controller.dart` serves as a singleton façade over the platform-specific Clash Meta engine, exposing methods like `init()`, `getTraffic()`, and `getConfig()`.
- **Riverpod providers** act as reactive intermediaries, with notifiers in `lib/providers/action.dart` invoking CoreController methods and updating UI state.
- The architecture enforces a **unidirectional data flow**: UI triggers actions → Actions call CoreController → Results update Riverpod state → Widgets rebuild automatically.
- **Lifecycle safety** is maintained by checking `coreController.isCompleted` and `isInit` before performing destructive operations like restarting the core.
- **State derivation** providers like `clashConfig` read directly from the CoreController to ensure UI always reflects the actual native engine configuration.

## Frequently Asked Questions

### What is the CoreController's primary responsibility?

The **CoreController** acts as a singleton façade that forwards all proxy-related operations to the native Go core via FFI on Android or socket services on desktop. It exposes a Dart-friendly async API while hiding platform-specific implementation details, allowing Riverpod providers to remain platform-agnostic.

### How does FlClash ensure UI updates when traffic changes?

The `CommonAction` notifier calls `coreController.getTraffic()` periodically, then updates `trafficsProvider` and `totalTrafficProvider` with the returned data. Because these are Riverpod state notifiers, any widget watching them automatically rebuilds when new traffic statistics arrive, creating a reactive pipeline from the native engine to the UI.

### Why does the codebase use Riverpod instead of other state management solutions?

FlClash uses **Riverpod** for its compile-time safety, testability, and ability to handle complex asynchronous state. The `@riverpod` annotations generate type-safe providers that can be easily mocked for testing, while the `keepAlive` feature ensures critical services like the CoreController connection persist across widget rebuilds without manual lifecycle management.

### What prevents the Core from being initialized multiple times?

The `CoreAction.initCore()` method checks `coreController.isInit` before calling `init()`. Additionally, `CoreAction.tryStartCore()` verifies `coreController.isCompleted` to ensure the Go core has finished its startup sequence before allowing restart operations. These guards prevent race conditions and resource leaks during the proxy lifecycle.