Plugin Development Guide

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?

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:

  1. Plugin Class - Extends PluginBase with lifecycle methods
  2. Plugin Manifest - Metadata describing the plugin
  3. 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

1

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
2

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',
);
3

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
  }
}
4

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
}
🔒 Storage Isolation

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

Performance

User Experience

Code Quality

💡 Testing Tip

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

Resources

Publishing Your Plugin

🚧 Coming Soon

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:

  1. Create your plugin in lib/plugins/your_plugin/
  2. Register it in lib/state/plugin_state.dart
  3. Add tests in test/plugins/
  4. Add user documentation in docs/help/plugins/
  5. Submit a pull request with your plugin

Need Help?

🎉 Share Your Plugin

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.