How the AppFlowy Plugin System Architecture Works in the Flutter Frontend

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.

// 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:

// 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:

// 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:

// 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:

// 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:

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:

// 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:

// 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:

// 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:

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:

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:

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:

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →