Question #77EasyArchitectureImportant

What is single ten design pattern ?

Answer

Overview

The Singleton design pattern ensures that a class has only one instance throughout the entire application lifecycle, and provides a global access point to that instance.

Think of it like a country's President — there can only be one at a time, and everyone refers to the same person.


When to Use Singleton

  • Database connections (only one connection pool)
  • App configuration / settings
  • Logger service
  • Network client (e.g., single
    text
    Dio
    instance)
  • Dependency injection containers

Basic Singleton in Dart

dart
class AppConfig {
  // Step 1: Static private instance
  static AppConfig? _instance;

  // Step 2: Private constructor
  AppConfig._privateConstructor();

  // Step 3: Public factory method to return the same instance
  factory AppConfig() {
    _instance ??= AppConfig._privateConstructor();
    return _instance!;
  }

  // Your actual fields
  String apiBaseUrl = 'https://api.example.com
  String apiKey = 'YOUR_API_KEY';
}

// Usage
void main() {
  final config1 = AppConfig();
  final config2 = AppConfig();

  print(identical(config1, config2)); // true — same instance!
}

Simplified Singleton (Using Static Instance)

dart
class Logger {
  // Create the single instance immediately
  static final Logger instance = Logger._internal();

  Logger._internal();

  void log(String message) {
    print('[LOG] ${DateTime.now()}$message');
  }
}

// Usage
Logger.instance.log('App started');
Logger.instance.log('User logged in');

Singleton for Network Client (Practical Example)

dart
import 'package:dio/dio.dart';

class ApiClient {
  static final ApiClient _instance = ApiClient._internal();
  late final Dio dio;

  factory ApiClient() => _instance;

  ApiClient._internal() {
    dio = Dio(BaseOptions(
      baseUrl: 'https://api.example.com
      connectTimeout: Duration(seconds: 10),
      receiveTimeout: Duration(seconds: 10),
    ));

    // Add interceptors once
    dio.interceptors.add(LogInterceptor(responseBody: true));
  }

  Future<dynamic> get(String path) async {
    final response = await dio.get(path);
    return response.data;
  }
}

// Usage anywhere in app
final data = await ApiClient().get('/users');

Thread-Safe Singleton (Dart Isolates)

Dart is single-threaded in its main isolate, so the basic singleton above is already thread-safe for most Flutter use cases. However, if you're using multiple isolates:

dart
class DatabaseHelper {
  static final DatabaseHelper _instance = DatabaseHelper._internal();

  factory DatabaseHelper() => _instance;
  DatabaseHelper._internal();

  // Lazy initialization of heavy resource
  Database? _db;

  Future<Database> get database async {
    _db ??= await _initDatabase();
    return _db!;
  }

  Future<Database> _initDatabase() async {
    return await openDatabase('app.db', version: 1);
  }
}

Singleton in GetX

dart
// Register as singleton
Get.put(ApiService());         // Eager — created immediately
Get.lazyPut(() => ApiService()); // Lazy — created on first access

// Access anywhere
final api = Get.find<ApiService>();

Singleton in get_it (Dependency Injection)

dart
final getIt = GetIt.instance;

// Register
getIt.registerSingleton<ApiService>(ApiService());

// Access anywhere
final service = getIt<ApiService>();

Pros and Cons

ProsCons
✅ Single shared instance — saves memory❌ Hard to unit test (global state)
✅ Global access point❌ Can hide dependencies (bad for DI)
✅ Lazy initialization possible❌ Violates Single Responsibility if overused
✅ Useful for shared resources❌ Can cause tight coupling

Common Mistakes

dart
// ❌ Wrong — not a singleton, creates new instance each time
class NotASingleton {
  static NotASingleton create() => NotASingleton();
}

// ✅ Correct — always returns same instance
class Singleton {
  static final Singleton _instance = Singleton._();
  factory Singleton() => _instance;
  Singleton._();
}

Best Practice: In Flutter, prefer using

text
GetIt
or
text
Riverpod
for managing singletons rather than rolling your own — they provide better lifecycle management and testability.