Learn how to create powerful plugins that extend Azyrom's functionality. This guide covers architecture, APIs, best practices, and examples for building production-quality plugins.
Overview
The Azyrom Plugin System allows developers to extend the design tool with custom features, automation, integrations, and tools. Plugins are built using Dart/Flutter and have full access to the canvas, elements, and user interface.
What Can Plugins Do?
- Canvas Operations - Create, read, update, delete elements
- Selection Management - Access and manipulate selected elements
- UI Integration - Show dialogs, notifications, custom panels
- Data Persistence - Store plugin data locally
- External Integration - Connect to APIs and external services
- Automation - Batch operations, workflows, generators
Plugin Types
| Type | Examples | Use Cases |
|---|---|---|
| Content Generators | Iconify, Unsplash, Chart Generator | Insert external content into designs |
| Data Tools | Content Reel | Generate placeholder text and data |
| Image Processing | Remove BG | AI-powered image manipulation |
| Design Utilities | Alignment tools, layout generators | Automate repetitive design tasks |
| Import/Export | Custom format converters | Support additional file formats |
| Integrations | Cloud storage, design systems | Connect to external services |
Plugin Architecture
Core Components
Every plugin consists of three main parts:
- Plugin Class - Extends
PluginBasewith lifecycle methods - Plugin Manifest - Metadata describing the plugin
- Plugin Context - Provides API access (Canvas, Selection, UI, Storage)
Lifecycle Methods
Plugins have four lifecycle stages:
| Method | When Called | Purpose |
|---|---|---|
onInit() |
Plugin first loaded | Initialize resources, services, configuration |
onActivate() |
User enables plugin | Show UI, register listeners, start services |
onDeactivate() |
User disables plugin | Hide UI, stop services, unregister listeners |
onDispose() |
Plugin uninstalled or app closing | Release all resources, save state |
Creating Your First Plugin
Create Plugin Directory
Create a new directory in lib/plugins/ for your plugin:
lib/plugins/my_plugin/ ├── my_plugin.dart # Main plugin class └── manifest.dart # Plugin metadata
Define Plugin Manifest
Create manifest.dart with plugin metadata:
import '../../models/plugin_manifest.dart'; const manifest = PluginManifest( id: 'my-plugin', name: 'My Plugin', version: '1.0.0', description: 'A helpful plugin', author: 'Your Name', category: 'utilities', tags: ['automation', 'tools'], permissions: [], mainFilePath: 'my_plugin.dart', className: 'MyPlugin', );
Implement Plugin Class
Create my_plugin.dart extending PluginBase:
import '../../services/plugin_base.dart'; import '../../services/plugin_context.dart'; class MyPlugin extends PluginBase { @override Future<void> onInit(PluginContext context) async { context.log('My plugin initialized'); } @override Future<void> onActivate() async { context.ui.showSuccess('My plugin activated!'); } @override Future<void> onDeactivate() async { context.log('My plugin deactivated'); } @override Future<void> onDispose() async { context.log('My plugin disposed'); } /// Your plugin methods Future<void> doSomethingAwesome() async { // Plugin logic here } }
Register Plugin
Add to lib/state/plugin_state.dart in the _registerPlugins method:
import '../plugins/my_plugin/my_plugin.dart'; import '../plugins/my_plugin/manifest.dart' as my_plugin; void _registerPlugins() { // Existing plugins... // Register your plugin manager.register(my_plugin.manifest, () => MyPlugin()); }
Plugin API Reference
Canvas API
Manipulate elements on the canvas:
// Add element to canvas final rect = CanvasElement( id: 'rect1', type: 'rectangle', x: 100, y: 100, width: 200, height: 150, ); context.canvas.addElement(rect); // Get element by ID final element = context.canvas.getElementById('rect1'); // Update element context.canvas.updateElement('rect1', element.copyWith(x: 150)); // Delete element context.canvas.deleteElement('rect1'); // Get all elements final elements = context.canvas.getAllElements(); // Duplicate element final duplicateId = context.canvas.duplicateElement('rect1'); // Get canvas size final size = context.canvas.getCanvasSize();
Selection API
Work with selected elements:
// Get selected element IDs final selection = context.selection.getSelection(); // Check if element is selected if (context.selection.isSelected('rect1')) { // Element is selected } // Set selection context.selection.setSelection(['rect1', 'rect2']); // Add to selection context.selection.addToSelection(['rect3']); // Remove from selection context.selection.removeFromSelection(['rect1']); // Clear selection context.selection.clearSelection(); // Get selection count final count = context.selection.getSelectionCount(); // Get first selected element final firstId = context.selection.getFirstSelected(); // Select all context.selection.selectAll(); // Invert selection context.selection.invertSelection();
UI API
Display notifications and dialogs:
// Show success notification context.ui.showSuccess('Operation completed!'); // Show error notification context.ui.showError('Something went wrong'); // Show info notification context.ui.showInfo('Processing...'); // Show warning notification context.ui.showWarning('Please save your work'); // Show loading indicator context.ui.showLoading('Loading data...'); // Hide loading indicator context.ui.hideLoading(); // Show custom dialog final result = await context.ui.showDialog<String>( MyCustomDialog(), );
Storage API
Persist plugin data locally:
// Save string await context.storage.setString('api_key', 'abc123'); // Get string final apiKey = await context.storage.getString('api_key'); // Save JSON (List or Map) await context.storage.setJson('config', { 'theme': 'dark', 'count': 42, }); // Get JSON final config = await context.storage.getJson('config'); // Save boolean await context.storage.setBool('enabled', true); // Get boolean final enabled = await context.storage.getBool('enabled'); // Save integer await context.storage.setInt('counter', 10); // Get integer final counter = await context.storage.getInt('counter'); // Remove item await context.storage.remove('api_key'); // Clear all plugin storage await context.storage.clear(); // Check if key exists if (await context.storage.containsKey('api_key')) { // Key exists }
Each plugin has its own isolated storage namespace. Plugins cannot access other plugins' data.
All keys are automatically prefixed with plugin.{pluginId}.
Complete Example: Element Multiplier Plugin
Here's a complete example that multiplies selected elements in a grid:
import '../../services/plugin_base.dart'; import '../../services/plugin_context.dart'; import '../../models/elements/canvas_element.dart'; class ElementMultiplierPlugin extends PluginBase { @override Future<void> onInit(PluginContext context) async { context.log('Element Multiplier initialized'); } @override Future<void> onActivate() async { context.ui.showSuccess('Element Multiplier ready!'); } /// Multiply selected elements in a grid Future<void> multiplyElements({ required int rows, required int columns, required double spacing, }) async { try { // Get selected elements final selectedIds = context.selection.getSelection(); if (selectedIds.isEmpty) { context.ui.showWarning('Please select at least one element'); return; } context.ui.showLoading('Creating grid...'); // Get the first selected element as template final templateId = selectedIds.first; final template = context.canvas.getElementById(templateId); if (template == null) { context.ui.showError('Selected element not found'); return; } // Calculate total dimensions final elementWidth = template.width; final elementHeight = template.height; // Create grid final newElements = <CanvasElement>[]; for (var row = 0; row < rows; row++) { for (var col = 0; col < columns; col++) { // Skip the original element position if (row == 0 && col == 0) continue; // Calculate position final x = template.x + col * (elementWidth + spacing); final y = template.y + row * (elementHeight + spacing); // Create copy final copy = template.copyWith( id: '${template.id}-$row-$col', x: x, y: y, ); newElements.add(copy); } } // Add all new elements to canvas context.canvas.addElements(newElements); context.ui.hideLoading(); context.ui.showSuccess( 'Created ${newElements.length} copies in ${rows}x$columns grid', ); context.log('Grid created: $rows x $columns'); } catch (e) { context.ui.hideLoading(); context.error('Error creating grid', e); context.ui.showError('Failed to create grid: $e'); } } @override Future<void> onDeactivate() async { context.log('Element Multiplier deactivated'); } @override Future<void> onDispose() async { context.log('Element Multiplier disposed'); } }
Best Practices
Error Handling
- Always wrap async operations in try-catch blocks
- Provide user-friendly error messages via
context.ui.showError() - Log errors for debugging via
context.error() - Handle edge cases (empty selection, invalid input, etc.)
Performance
- Batch operations - Use
addElements()instead of multipleaddElement()calls - Show loading indicators for operations taking > 200ms
- Avoid blocking the UI thread - Use async/await for heavy operations
- Cache frequently accessed data in memory
User Experience
- Provide clear feedback - Show success/error notifications
- Validate input early - Check selection before processing
- Use descriptive messages - Tell users what happened and why
- Support undo/redo - All canvas operations are automatically undoable
Code Quality
- Use meaningful names for variables and methods
- Add documentation with
///comments - Follow Dart conventions - Use lowerCamelCase, etc.
- Write tests for your plugin logic
Use the plugin test pattern from the test suite. Create mock APIs to test your plugin logic
without running the full app. See test/plugins/ for examples.
Plugin Manifest Reference
| Field | Type | Required | Description |
|---|---|---|---|
id |
String | ✅ | Unique identifier (kebab-case, e.g. "my-plugin") |
name |
String | ✅ | Display name shown in UI |
version |
String | ✅ | Semantic version (e.g. "1.0.0") |
description |
String | ✅ | Short description for marketplace |
author |
String | ✅ | Author name or organization |
category |
String | ✅ | Category: utilities, icons, images, content, etc. |
mainFilePath |
String | ✅ | Main file name (e.g. "my_plugin.dart") |
className |
String | ✅ | Plugin class name (e.g. "MyPlugin") |
tags |
List<String> | ❌ | Search tags for discovery |
permissions |
List<String> | ❌ | Required permissions (currently unused) |
Testing Your Plugin
Unit Testing
Create tests in test/plugins/your_plugin_test.dart:
import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; void main() { group('MyPlugin Tests', () { late MyPlugin plugin; late MockPluginContext context; setUp(() { context = MockPluginContext(); plugin = MyPlugin(); }); test('initializes correctly', () async { await plugin.internalInit(context, manifest); expect(plugin.isInitialized, isTrue); }); test('handles empty selection', () async { when(context.selection.getSelection()) .thenReturn([]); await plugin.doSomething(); verify(context.ui.showWarning(any)).called(1); }); }); }
Manual Testing Checklist
- ✅ Plugin activates without errors
- ✅ All features work as expected
- ✅ Error handling works correctly
- ✅ UI notifications are clear and helpful
- ✅ Performance is acceptable
- ✅ Works with 1000+ elements
- ✅ Storage persists across restarts
- ✅ Plugin deactivates cleanly
Resources
- User Plugin Guide - End-user documentation
- Example Plugins -
lib/plugins/iconify/,lib/plugins/unsplash/ - Test Examples -
test/plugins/ - Plugin Base -
lib/services/plugin_base.dart - Plugin Context -
lib/services/plugin_context.dart - Plugin Manager -
lib/services/plugin_manager.dart
Publishing Your Plugin
A plugin marketplace and publishing system is in development. For now, plugins must be included in the main app codebase and compiled in.
To include your plugin in the main app:
- Create your plugin in
lib/plugins/your_plugin/ - Register it in
lib/state/plugin_state.dart - Add tests in
test/plugins/ - Add user documentation in
docs/help/plugins/ - Submit a pull request with your plugin
Need Help?
- Discord - Join the developer community
- GitHub Issues - Report bugs or request features
- Email - plugins@azyrom.com
Built something awesome? Share it with the community! Plugins that solve common problems and maintain high quality may be included in the official plugin library.