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_pluginfolder, and returns aDynamicPluginLibrary(Iterable<FlowyDynamicPlugin>)lookup(name): Retrieves a specific plugin by its identifieraddPlugin(plugin): Copies a compiled plugin into the storage locationremovePlugin(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_plugindirectories and compiles theme JSON files intoAppThemeobjects containingFlowyColorSchemedefinitions 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
PluginTypeenum and switch-based decoding inFlowyDynamicPluginprovide 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →