Instancer

pub package Donate issues-closed issues-open Contributions

A simple, lightweight factory registry for Dart that makes creating instances of registered types easy and flexible.

Perfect for dependency injection, prototyping, configuration management, and testing!

Features

โœจ Simple API: Just 5 intuitive methods to learn
๐ŸŽฏ Type-safe: Full generic type support
๐Ÿชถ Lightweight: Zero dependencies
๐Ÿ”ง Flexible: Register any factory function
๐Ÿงช Testable: Easy to clear and reset for testing

Getting started

Add this to your pubspec.yaml:

dependencies:
  instancer: ^1.0.0

Then import it:

import 'package:instancer/instancer.dart';

Usage

๐Ÿš€ Quick Start

import 'package:instancer/instancer.dart';

class User {
  final String name;
  final int age;
  
  User({required this.name, required this.age});
}

void main() {
  // 1๏ธโƒฃ Register a factory function
  Instancer.register<User>(() => User(name: 'Guest', age: 18));
  
  // 2๏ธโƒฃ Create instances anywhere in your code
  final user1 = Instancer.create<User>();
  final user2 = Instancer.create<User>();
  
  print(user1.name); // Output: Guest
  print(identical(user1, user2)); // Output: false (different instances)
}

๐Ÿ“ฆ Real-World Examples

Example 1: Configuration Management

class AppConfig {
  final String apiUrl;
  final bool debugMode;
  final int timeout;
  
  AppConfig({
    required this.apiUrl, 
    required this.debugMode,
    required this.timeout,
  });
}

void main() {
  // Development environment
  Instancer.register<AppConfig>(
    () => AppConfig(
      apiUrl: 'http://localhost:3000',
      debugMode: true,
      timeout: 30,
    ),
  );
  
  // Use it anywhere in your app
  final config = Instancer.create<AppConfig>();
  print(config.apiUrl); // http://localhost:3000
  
  // Switch to production (just re-register)
  Instancer.register<AppConfig>(
    () => AppConfig(
      apiUrl: 'https://api.production.com',
      debugMode: false,
      timeout: 10,
    ),
  );
  
  final prodConfig = Instancer.create<AppConfig>();
  print(prodConfig.apiUrl); // https://api.production.com
}

Example 2: Dependency Injection

class Database {
  void query(String sql) => print('Executing: $sql');
}

class UserRepository {
  final Database database;
  
  UserRepository({required this.database});
  
  void save(String name) {
    database.query("INSERT INTO users (name) VALUES ('$name')");
  }
}

class UserService {
  final UserRepository repository;
  
  UserService({required this.repository});
  
  void createUser(String name) {
    print('Creating user: $name');
    repository.save(name);
  }
}

void main() {
  // Register dependencies in order
  Instancer.register<Database>(() => Database());
  
  Instancer.register<UserRepository>(
    () => UserRepository(database: Instancer.create<Database>()),
  );
  
  Instancer.register<UserService>(
    () => UserService(repository: Instancer.create<UserRepository>()),
  );
  
  // Use the service
  final userService = Instancer.create<UserService>();
  userService.createUser('Alice');
  // Output:
  // Creating user: Alice
  // Executing: INSERT INTO users (name) VALUES ('Alice')
}

Example 3: Testing with Mocks

abstract class ApiClient {
  Future<String> fetchData();
}

class RealApiClient implements ApiClient {
  @override
  Future<String> fetchData() async {
    // Real network call
    return 'Real data from server';
  }
}

class MockApiClient implements ApiClient {
  @override
  Future<String> fetchData() async {
    // Mock data for testing
    return 'Mock data';
  }
}

void main() {
  // Production code
  Instancer.register<ApiClient>(() => RealApiClient());
  
  // In your tests, just re-register with mock
  Instancer.register<ApiClient>(() => MockApiClient());
  
  final client = Instancer.create<ApiClient>();
  client.fetchData().then(print); // Output: Mock data
  
  // Clean up after tests
  Instancer.clear();
}

Example 4: Prototype Pattern

class EmailTemplate {
  final String subject;
  final String body;
  final List<String> recipients;
  
  EmailTemplate({
    required this.subject,
    required this.body,
    this.recipients = const [],
  });
  
  @override
  String toString() => 'Email: $subject to ${recipients.length} recipients';
}

void main() {
  // Register a prototype template
  Instancer.register<EmailTemplate>(
    () => EmailTemplate(
      subject: 'Welcome!',
      body: 'Welcome to our service',
      recipients: ['[email protected]'],
    ),
  );
  
  // Create multiple emails from the same template
  final email1 = Instancer.create<EmailTemplate>();
  final email2 = Instancer.create<EmailTemplate>();
  
  print(email1); // Email: Welcome! to 1 recipients
  print(email2); // Email: Welcome! to 1 recipients
  print('Same instance? ${identical(email1, email2)}'); // false
}

Example 5: Factory with State

class Logger {
  static int _instanceCount = 0;
  final int id;
  
  Logger() : id = ++_instanceCount;
  
  void log(String message) {
    print('[Logger $id] $message');
  }
}

void main() {
  // Each creation increases the counter
  Instancer.register<Logger>(() => Logger());
  
  final logger1 = Instancer.create<Logger>();
  final logger2 = Instancer.create<Logger>();
  final logger3 = Instancer.create<Logger>();
  
  logger1.log('First message');   // [Logger 1] First message
  logger2.log('Second message');  // [Logger 2] Second message
  logger3.log('Third message');   // [Logger 3] Third message
}

API Reference

register<T>(T Function() factory)

Registers a factory function for type T.

Instancer.register<User>(() => User(name: 'John', age: 25));

Parameters:

  • factory: A function that returns a new instance of type T

create<T>()

Creates a new instance of type T using the registered factory.

final user = Instancer.create<User>();

Returns: A new instance of type T

Throws: StateError if no factory is registered for type T

isRegistered<T>()

Checks if a factory is registered for type T.

if (Instancer.isRegistered<User>()) {
  print('User factory is registered!');
}

Returns: true if registered, false otherwise

unregister<T>()

Removes the factory for type T.

final removed = Instancer.unregister<User>();
print('Factory removed: $removed');

Returns: true if a factory was removed, false if none existed

clear()

Removes all registered factories. Useful for testing or resetting state.

Instancer.clear();

count

Returns the number of registered factories.

print('Total registered factories: ${Instancer.count}');

Why Instancer?

Feature Instancer Traditional Singleton Factory Pattern
No inheritance required โœ… โŒ โœ…
No code generation โœ… โœ… โœ…
Multiple instances โœ… โŒ โœ…
Easy to test โœ… โŒ โœ…
Dynamic registration โœ… โŒ โŒ
Zero dependencies โœ… โœ… โœ…

Benefits:

  • No inheritance required: Your classes don't need to extend anything
  • No code generation: Works without build_runner or code generation
  • Pure Dart: No platform-specific code, works everywhere (Flutter, CLI, Web)
  • Flexible: Register simple constructors or complex initialization logic
  • Testable: Easy to swap implementations for mocking in tests
  • Simple: Only 5 methods to learn - register, create, isRegistered, unregister, clear

Common Use Cases

โœ… Dependency Injection - Register and inject dependencies
โœ… Configuration Management - Switch between dev/prod configs
โœ… Testing - Replace real implementations with mocks
โœ… Prototype Pattern - Create multiple instances from templates
โœ… Factory Pattern - Centralized instance creation
โœ… Plugin Systems - Register and create plugin instances

Tips & Best Practices

1. Register early, use anywhere

// In your app initialization
void setupDependencies() {
  Instancer.register<Database>(() => SqliteDatabase());
  Instancer.register<ApiClient>(() => HttpApiClient());
  Instancer.register<AuthService>(() => AuthService());
}

// Use anywhere in your app
final auth = Instancer.create<AuthService>();

2. Use for environment-specific configurations

void setupConfig(Environment env) {
  if (env == Environment.dev) {
    Instancer.register<Config>(() => DevConfig());
  } else {
    Instancer.register<Config>(() => ProdConfig());
  }
}

3. Clean up in tests

void main() {
  setUp(() {
    Instancer.register<Service>(() => MockService());
  });
  
  tearDown(() {
    Instancer.clear(); // Always clean up!
  });
  
  test('service works', () {
    final service = Instancer.create<Service>();
    // Test...
  });
}

Contributing

Contributions are welcome! Please read our CONTRIBUTING.md for details.

  1. Check existing issues
  2. Create a new issue or submit a pull request
  3. Follow the existing code style

License

MIT License - see the LICENSE file for details.

Author

Created with โค๏ธ by Uproid


Like this package? Give it a โญ on GitHub!

Libraries

instancer
A simple and flexible factory registry for Dart.