The problems this solves
Every Flutter app rebuilds the same networking layer, and gets the same four things slightly wrong.
Two widgets ask for the same thing at once. A header shows the user's avatar, a settings tile shows their name, both mount together, and GET /users/me goes out twice. deo_emerges collapses identical in-flight GETs into one request automatically. No setup.
The token expires mid-screen. Six requests get a 401 at the same moment, six refresh calls race, and one of them wins while the others invalidate the token they just got. The auth interceptor refreshes once and replays the rest.
JSON to models is written by hand, badly. response.data['user']['name'] as String scattered through the codebase, and one null crashes the screen. getJson decodes into your model and tells you clearly when it was the decoding that failed rather than the network.
The user leaves the screen and the request keeps going. Then it completes, calls setState, and throws on a disposed widget. Named cancel tokens make cancelling on dispose one line.
Install
dependencies:
deo_emerges: ^0.2.0
Three dependencies: dio, logger, crypto. Nothing else. It does not pull in a state management library, and it never will.
Quick start
import 'package:deo_emerges/deo_emerges.dart';
final client = DeoClient(
config: DeoConfig(baseUrl: 'https://api.example.com'),
);
final response = await client.get('/users/me');
print(response.data);
Typed responses
class User {
User({required this.id, required this.name});
final String id;
final String name;
factory User.fromJson(Map<String, dynamic> json) => User(
id: (json['id'] ?? '') as String,
name: (json['name'] ?? '') as String,
);
}
final user = await client.getJson('/users/me', (json) => User.fromJson(json));
print(user.name);
If your model throws while parsing, you get a DeoError that says decoding failed. You will not spend an hour looking at the wrong layer.
Deduplication
// Two widgets, same moment, one network call.
final a = client.get('/users/me');
final b = client.get('/users/me');
await Future.wait([a, b]);
Identical path plus identical query parameters, already in flight, returns the same future. Query order does not matter: ?a=1&b=2 and ?b=2&a=1 are the same request.
GET only. POST, PUT, PATCH and DELETE change state on the server, so they are never shared.
Opt out per call when you need a genuinely fresh read:
await client.get('/feed', dedupe: false);
Concurrent requests
final results = await client.concurrent([
() => client.get('/users/me'),
() => client.get('/notifications'),
() => client.get('/settings'),
]);
Cancelling
await client.get('/search', queryParameters: {'q': term}, cancelToken: 'search');
@override
void dispose() {
client.cancelRequest('search');
super.dispose();
}
Auth with refresh
client.addInterceptor(
AuthInterceptor(
getToken: () => storage.read('access_token'),
refreshToken: () => authRepository.refresh(),
),
);
Caching
client.addInterceptor(CacheInterceptor(maxAge: const Duration(minutes: 5)));
Wiring it to your state tool
Version 0.2.0 removed the built-in Riverpod, Provider and Bloc adapters. They forced three competing state libraries on every install, and wiring your own takes five lines.
Riverpod
final clientProvider = Provider((ref) => DeoClient(config: DeoConfig(baseUrl: baseUrl)));
final userProvider = FutureProvider.autoDispose((ref) async {
return ref.watch(clientProvider).getJson('/users/me', User.fromJson);
});
Bloc / Cubit
class UserCubit extends Cubit<AsyncValue<User>> {
UserCubit(this._client) : super(const AsyncLoading());
final DeoClient _client;
Future<void> load() async {
try {
emit(AsyncData(await _client.getJson('/users/me', User.fromJson)));
} on DeoError catch (e) {
emit(AsyncError(e));
}
}
}
Provider
class UserModel extends ChangeNotifier {
UserModel(this._client);
final DeoClient _client;
User? user;
Future<void> load() async {
user = await _client.getJson('/users/me', User.fromJson);
notifyListeners();
}
}
Same client, any state tool, no dependency on any of them.
Custom interceptors
client.addInterceptor(MyCustomInterceptor());
SSL pinning
final client = DeoClient(
config: DeoConfig(
baseUrl: 'https://api.example.com',
validateCertificate: true,
certificates: ['certificate1', 'certificate2'],
),
);
Errors
Everything throws DeoError, so you catch one type:
try {
await client.get('/users/me');
} on DeoError catch (e) {
print(e.message);
print(e.statusCode);
}
Platforms
Android, iOS, web, macOS, Windows and Linux, and it is WebAssembly compatible. Pure Dart over Dio, no platform channels, no dart:io.
Contributing
See CONTRIBUTING.md. Issues and pull requests are welcome.
Security
See SECURITY.md. Please report vulnerabilities privately rather than in a public issue.
License
MIT © Tisankan Jeyakumar