# How the AppFlowy Plugin System Architecture Works in the Flutter Frontend

> Discover the AppFlowy plugin system architecture. Learn how runtime extensions are discovered compiled and managed reactively within the Flutter frontend for seamless integration.

- Repository: [AppFlowy-IO/AppFlowy](https://github.com/AppFlowy-IO/AppFlowy)
- Tags: architecture
- Published: 2026-03-03

---

**AppFlowy's plugin system architecture enables runtime extension through `.flowy_plugin` directories that are discovered by `PluginLocationService`, compiled into `AppTheme` objects by `FlowyDynamicPlugin`, and managed reactively via `FlowyPluginService` and `DynamicPluginBloc`.**

The AppFlowy Flutter frontend implements a dynamic plugin system architecture that allows developers to install custom themes at runtime without recompiling the application. Located in `frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/`, this system uses a three-layer pipeline of discovery, validation, and state management to integrate external resources seamlessly.

## Plugin Discovery and Storage

### PluginLocationService and File System Structure

The architecture begins with **PluginLocationService**, which abstracts the physical storage location of all plugins.

```dart
// frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/location_service.dart
class PluginLocationService {
  const PluginLocationService({
    required Future<Directory> fallback,
  }) : _fallback = fallback;

  final Future<Directory> _fallback;

  Future<Directory> get fallback async => _fallback;
  Future<Directory> get location async => fallback;
}

```

By default, plugins reside in the platform-specific application documents directory, as configured in `FlowyPluginService.initialize`:

```dart
// frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/plugin_service.dart
_locationService = PluginLocationService(
  fallback: getApplicationDocumentsDirectory(),
);

```

Developers can override this location via dependency injection for testing or custom deployment scenarios.

## Plugin Compilation and Validation

### Recognizing Valid Plugin Directories

The system identifies valid plugins through the `FlowyDynamicPlugin.isPlugin` static method, which checks for the `.flowy_plugin` extension:

```dart
// frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart
static bool isPlugin(FileSystemEntity entity) =>
    entity is Directory && p.extension(entity.path).contains(ext);
static const String ext = 'flowy_plugin';

```

### Supported Plugin Types

Currently, the architecture supports a single plugin category defined in `PluginType`:

```dart
// frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/plugin_type.dart
enum PluginType { theme }

```

Future extensions could add additional types to this enumeration.

### Decoding Theme Plugins

The `FlowyDynamicPlugin.decode` factory method validates and compiles plugin directories into in-memory models:

```dart
// frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart
static Future<FlowyDynamicPlugin> decode({required Directory src}) async {
  if (!isPlugin(src)) {
    throw PluginCompilationException(
      'The plugin directory must have the extension `.flowy_plugin`.',
    );
  }
  final type = PluginType.from(src: src);
  switch (type) {
    case PluginType.theme:
      return _theme(src: src);
  }
}

```

For theme plugins, the expected directory structure contains two JSON files:

```

my_theme.flowy_plugin/
 ├─ my_theme.light.json
 └─ my_theme.dark.json

```

The private `_theme` method parses these files into `FlowyColorScheme` objects and constructs an `AppTheme` instance:

```dart
final theme = AppTheme(
  themeName: name,
  builtIn: false,
  lightTheme: lightTheme,
  darkTheme: darkTheme,
);
return FlowyDynamicPlugin._(
  name: theme.themeName,
  path: src.path,
  theme: theme,
);

```

### In-Memory Representation

The compiled theme data resides in the **AppTheme** class, which merges with built-in themes:

```dart
// frontend/appflowy_flutter/packages/flowy_infra/lib/theme.dart
class AppTheme {
  final bool builtIn;
  final String themeName;
  final FlowyColorScheme lightTheme;
  final FlowyColorScheme darkTheme;
  // ...
}

```

## Runtime Management and Reactive State

### FlowyPluginService API

The **FlowyPluginService** class provides the core asynchronous API for plugin management:

- `plugins`: Scans the location directory, decodes each `.flowy_plugin` folder, and returns a `DynamicPluginLibrary` (`Iterable<FlowyDynamicPlugin>`)
- `lookup(name)`: Retrieves a specific plugin by its identifier
- `addPlugin(plugin)`: Copies a compiled plugin into the storage location
- `removePlugin(plugin)`: Deletes the plugin directory from disk

The `plugins` getter implementation demonstrates the compilation pipeline:

```dart
// frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/plugin_service.dart
Future<DynamicPluginLibrary> get plugins async {
  final List<FlowyDynamicPlugin> compiled = [];
  for (final src in await _targets) {
    final plugin = await FlowyDynamicPlugin.decode(src: src);
    compiled.add(plugin);
  }
  return compiled;
}

```

### BLoC Integration for Reactive UI

The UI layer interacts with the plugin system through **DynamicPluginBloc**, which emits `DynamicPluginState` in response to `DynamicPluginEvent` actions:

```dart
// frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_bloc.dart
class DynamicPluginBloc extends Bloc<DynamicPluginEvent, DynamicPluginState> {
  DynamicPluginBloc({FilePicker? filePicker})
      : state(const DynamicPluginState.uninitialized()) {
    on<DynamicPluginEvent>(dispatch);
    add(DynamicPluginEvent.load());
  }
}

```

The BLoC handles three primary operations:

- **Load**: Dispatches on initialization to populate the plugin library
- **Add**: Triggers a file picker, validates the selection, copies it to the plugin directory, and refreshes state
- **Remove**: Deletes a plugin by name and emits an updated state

## Working with the Plugin System

### Listing All Available Themes

To retrieve both built-in and plugin themes, use `AppTheme.themes`:

```dart
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra/plugins/service/plugin_service.dart';

Future<void> printAllThemes() async {
  final service = FlowyPluginService.instance;
  final themes = await AppTheme.themes(service);
  for (final theme in themes) {
    print('${theme.themeName} (built-in: ${theme.builtIn})');
  }
}

```

### Installing Custom Theme Plugins

Programmatically trigger the installation flow through the BLoC:

```dart
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void addCustomTheme(BuildContext context) {
  context.read<DynamicPluginBloc>().add(const DynamicPluginEvent.addPlugin());
}

```

This invokes `FlowyPluginService.pick()` to open a directory chooser, validates the selection via `FlowyDynamicPlugin.decode`, copies the folder to the plugin location, and emits a refreshed `DynamicPluginState.ready`.

### Uninstalling Plugins

Remove plugins by name using the BLoC:

```dart
void removeTheme(BuildContext context, String themeName) {
  context
      .read<DynamicPluginBloc>()
      .add(DynamicPluginEvent.removePlugin(name: themeName));
}

```

### Building Reactive Theme Selectors

Widgets automatically reflect plugin changes by subscribing to `DynamicPluginBloc`:

```dart
BlocBuilder<DynamicPluginBloc, DynamicPluginState>(
  builder: (context, state) {
    if (state is DynamicPluginState.ready) {
      final plugins = state.plugins;
      final themes = plugins
          .where((p) => p.theme != null)
          .map((p) => p.theme!)
          .toList()
        ..addAll(AppTheme.builtins);

      return DropdownButton<String>(
        value: selectedTheme,
        items: themes.map((t) => DropdownMenuItem(
          value: t.themeName,
          child: Text(t.themeName),
        )).toList(),
        onChanged: (value) {
          // Apply selected theme logic
        },
      );
    }
    return const CircularProgressIndicator();
  },
);

```

## Summary

- **PluginLocationService** abstracts the file system storage location, defaulting to the application documents directory but supporting custom overrides via `setLocation`.
- **FlowyDynamicPlugin.decode** validates `.flowy_plugin` directories and compiles theme JSON files into `AppTheme` objects containing `FlowyColorScheme` definitions for light and dark modes.
- **FlowyPluginService** provides the core API for scanning (`plugins`), retrieving (`lookup`), installing (`addPlugin`), and uninstalling (`removePlugin`) plugins.
- **DynamicPluginBloc** exposes a reactive interface that emits state changes when plugins load, install, or uninstall, enabling Flutter widgets to rebuild automatically.
- The architecture currently supports **theme plugins** exclusively, but the `PluginType` enum and switch-based decoding in `FlowyDynamicPlugin` provide extension points for future plugin categories.

## Frequently Asked Questions

### What directory structure does a valid AppFlowy theme plugin require?

A valid theme plugin must be a directory ending with the `.flowy_plugin` extension, containing exactly two JSON files: `[name].light.json` and `[name].dark.json`. These files define the `FlowyColorScheme` objects for light and dark modes respectively. The `FlowyDynamicPlugin.isPlugin` method validates the extension, while the private `_theme` decoder inside `FlowyDynamicPlugin.decode` parses the JSON contents.

### How does the plugin system distinguish between built-in and dynamic themes?

The `AppTheme` class includes a boolean `builtIn` field set to `false` for all dynamically loaded plugins and `true` for compiled-in themes. When `AppTheme.themes()` aggregates the available themes, it merges the results from `FlowyPluginService.plugins` (dynamic) with `AppTheme.builtins` (static), allowing the UI to display both sources seamlessly.

### Can I load plugins from a custom directory outside the default documents folder?

Yes. While `FlowyPluginService.initialize` configures `PluginLocationService` with `getApplicationDocumentsDirectory()` as the default fallback, you can inject a custom `PluginLocationService` via `setLocation` that returns any `Directory` instance. This enables testing scenarios, enterprise deployments with shared network storage, or sandboxed environments.

### What happens if a plugin directory fails validation during decoding?

If `FlowyDynamicPlugin.decode` encounters a directory without the `.flowy_plugin` extension, it throws a `PluginCompilationException` with the message "The plugin directory must have the extension `.flowy_plugin`." Similarly, if the internal `_theme` decoder cannot parse the required JSON files, the compilation fails and the plugin is excluded from the `DynamicPluginLibrary` returned by `FlowyPluginService.plugins`.