Dart Singleton design pattern in network service

A Singleton is a design pattern used in software engineering to ensure that a class has only one instance and provides a global point of access to that instance. This pattern is part of the Gang of Four (GoF) design patterns and falls under the creational design patterns category. The primary goal of the Singleton pattern is to control the instantiation of a class, ensuring that there is only one instance and providing a mechanism to access that instance from any point in the application.
Key features of a Singleton:
- Private Constructor: The Singleton class typically has a private constructor to prevent external instantiation of the class.
- Private Instance Variable: The Singleton class usually contains a private static variable to hold the single instance of the class.
- Static Method or Property for Access: It provides a static method or property that allows clients to access the single instance of the class. This method is often named something like
getInstance()or simplyinstance.
class Singleton {
private static Singleton instance;
private Singleton() {
// Private constructor to prevent instantiation.
}
public static Singleton getInstance() {
// Lazily create the instance if it doesn't exist.
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
In Dart, a singleton is a design pattern that ensures a class has only one instance and provides a global point of access to that instance.
this is A NetworkCommon “ webService ” with
dependencies:
dio: ^4.0.0
Now, let’s create a NetworkCommon singleton:
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
class NetworkCommon {
static final NetworkCommon _singleton = NetworkCommon._internal();
static const String baseUrl = "https://api.com/";
factory NetworkCommon() {
return _singleton;
}
NetworkCommon._internal();
final JsonDecoder _decoder = const JsonDecoder();
dynamic decodeResp(d) {
if (d is Response) {
final dynamic jsonBody = d.data;
final statusCode = d.statusCode;
if (statusCode! < 200 || statusCode >= 300 || jsonBody == null) {
throw Exception("statusCode: $statusCode");
}
if (jsonBody is String && jsonBody.isNotEmpty) {
return _decoder.convert(jsonBody);
} else {
return jsonBody;
}
} else {
throw d;
}
}
Dio get dio {
Dio dio = Dio();
dio.options.baseUrl = baseUrl;
dio.options.connectTimeout = const Duration(milliseconds: 5000);
dio.options.receiveTimeout = const Duration(milliseconds: 3000);
// dio.interceptors.add(alice.getDioInterceptor());
dio.interceptors.add(LogInterceptor(responseBody: true, requestBody: true));
dio.interceptors
.add(InterceptorsWrapper(onRequest: (options, handler) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString('token');
if (token != null) {
if (options.path != "auth/signin") {
options.headers["Authorization"] = "Bearer $token";
}
}
print("Pre request:${options.method},${options.baseUrl}${options.path}");
print("Pre request:${options.headers.toString()}");
return handler.next(options);
}, onResponse: (response, handler) async {
final int? statusCode = response.statusCode;
if (statusCode! < 300) {
if (response.requestOptions.path == "auth/signin") {
final SharedPreferences prefs = await SharedPreferences.getInstance();
JsonEncoder _encoder = const JsonEncoder();
final resultContainer = response.data;
prefs.setString("token", resultContainer["token"]);
prefs.setString(
"user", _encoder.convert((resultContainer["user"] as Map)));
prefs.setBool("isLogined", true);
}
} else if (response.requestOptions.path == "/users/profile") {
final SharedPreferences prefs = await SharedPreferences.getInstance();
JsonEncoder encoder = const JsonEncoder();
final resultContainer = response.data;
prefs.setString(
"user", encoder.convert((resultContainer["user"] as Map)));
}
print(statusCode);
print(
"Response From:${response.requestOptions.method},${response.requestOptions.baseUrl}${response.requestOptions.path}");
print("Response From:${response.toString()}");
return handler.next(response); // continue
}, onError: (DioException e, handler) {
return handler.next(e);
}));
return dio;
}
}
Creating a NetworkCommon in Dart using a singleton pattern can be useful to ensure that you have a single point of access to the service and manage the state of the connection efficiently. Below is an example of a simple NetworkCommon implemented as a lazy singleton in Dart.
you may need to handle more complex tasks like error handling, authentication, and other considerations.
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
class NetworkCommon {
static NetworkCommon? _singleton;
static const String baseUrl = "https://api.com/";
factory NetworkCommon() {
_singleton ??= NetworkCommon._internal();
return _singleton!;
}
NetworkCommon._internal();
final JsonDecoder _decoder = const JsonDecoder();
dynamic decodeResp(d) {
if (d is Response) {
final dynamic jsonBody = d.data;
final statusCode = d.statusCode;
if (statusCode! < 200 || statusCode >= 300 || jsonBody == null) {
throw Exception("statusCode: $statusCode");
}
if (jsonBody is String && jsonBody.isNotEmpty) {
return _decoder.convert(jsonBody);
} else {
return jsonBody;
}
} else {
throw d;
}
}
Dio get dio {
Dio dio = Dio();
dio.options.baseUrl = baseUrl;
dio.options.connectTimeout = const Duration(milliseconds: 5000);
dio.options.receiveTimeout = const Duration(milliseconds: 3000);
// dio.interceptors.add(alice.getDioInterceptor());
dio.interceptors.add(LogInterceptor(responseBody: true, requestBody: true));
dio.interceptors
.add(InterceptorsWrapper(onRequest: (options, handler) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString('token');
if (token != null) {
if (options.path != "auth/signin") {
options.headers["Authorization"] = "Bearer $token";
}
}
print("Pre request:${options.method},${options.baseUrl}${options.path}");
print("Pre request:${options.headers.toString()}");
return handler.next(options);
}, onResponse: (response, handler) async {
final int? statusCode = response.statusCode;
if (statusCode! < 300) {
if (response.requestOptions.path == "auth/signin") {
final SharedPreferences prefs = await SharedPreferences.getInstance();
JsonEncoder _encoder = const JsonEncoder();
final resultContainer = response.data;
prefs.setString("token", resultContainer["token"]);
prefs.setString(
"user", _encoder.convert((resultContainer["user"] as Map)));
prefs.setBool("isLogined", true);
}
} else if (response.requestOptions.path == "/users/profile") {
final SharedPreferences prefs = await SharedPreferences.getInstance();
JsonEncoder encoder = const JsonEncoder();
final resultContainer = response.data;
prefs.setString(
"user", encoder.convert((resultContainer["user"] as Map)));
}
print(statusCode);
print(
"Response From:${response.requestOptions.method},${response.requestOptions.baseUrl}${response.requestOptions.path}");
print("Response From:${response.toString()}");
return handler.next(response); // continue
}, onError: (DioException e, handler) {
return handler.next(e);
}));
return dio;
}
}
While the Singleton pattern is a widely used design pattern, it is considered by some as an antipattern in certain contexts. Here are some reasons why Singleton might be considered an antipattern:
- Global State: Singletons provide a global point of access to their instance, which can lead to shared state across the application. Global state can make the code harder to reason about and maintain, as changes in one part of the application can affect other parts unexpectedly.
- Testability: Code that relies on Singletons can be challenging to test in isolation. Since Singletons often have a global impact, it may be difficult to write unit tests that don’t interfere with each other.
- Dependency Injection: Singleton can create tight coupling between components, making it harder to use dependency injection for providing dependencies to classes. Dependency injection is generally considered a good practice for managing dependencies and promoting testability.
- Concurrency Issues: In multithreaded environments, the Singleton pattern may introduce synchronization issues. Lazy initialization (creating the instance only when needed) can lead to race conditions if not properly handled.
- Hard to Subclass: Subclassing a Singleton can be challenging because the constructor is often private. This can limit the flexibility of the code, especially when trying to extend or customize the behavior of the Singleton.
- Hidden Dependencies: Singletons can hide dependencies by providing a global point of access to their instance. This can make it difficult to understand and reason about the dependencies of a class.
- Difficult to Mock: In unit testing, it’s common to replace real dependencies with mock objects. However, Singletons can be difficult to mock or replace because they often tightly couple themselves with the global state.
Despite these concerns, the Singleton pattern can still be useful in certain situations, such as when there is a genuine need for a single point of access to a resource, and when careful consideration is given to its usage and potential downsides. It’s essential to weigh the trade-offs and consider alternative patterns, like dependency injection, where appropriate. Ultimately, the decision to use or avoid the Singleton pattern depends on the specific requirements and context of the application.
Resources
Originally published on Medium on January 25, 2024.
About the author
- Software Engineer FullStack Developer " nest-js & nuxt js (vue.js) & Flutter"Mobile" "
Comments
Join the discussionNo comments yet. Be the first to comment.