By Abdelrahman Saed — Senior Mobile Engineer. Last updated: 2026-08-10.
Use Dio for any app with non-trivial networking: interceptors, token refresh, file uploads, request cancellation, timeout policies, or retry logic. Use the http package for one-off scripts or tiny apps with a couple of GETs.
http is the official, minimal Dart HTTP client — fine and boring for simple cases. Dio is a richer client built for real apps: interceptors (auth, logging, retry), CancelToken, FormData for multipart, and consistent error handling via DioException. In a 50+ module production app, Dio's interceptor stack is load-bearing infrastructure.
| Feature | Dio | http |
|---|---|---|
| Interceptors | First-class | None (DIY wrapper) |
| Cancellation | CancelToken | None |
| Timeouts | Configurable per-request | Global only |
| Multipart / FormData | Ergonomic + progress | MultipartRequest (basic) |
| Retry logic | Interceptor or built-in | DIY |
| Auth / token refresh | Interceptor pattern | Manual per call |
| Error model | DioException with type enum | ClientException (basic) |
| Streaming response | Yes | Yes (via HttpClient) |
| Best for | Real apps with networking complexity | Scripts, tiny apps |
http is intentionally minimal: get, post, put, delete, head, patch. You get a Response with a body string/bytes. No interceptors, no cancellation tokens, no built-in retry.
Dio wraps a similar core with a much richer API: interceptors (request/response/error), CancelToken, FormData (multipart uploads), configurable timeouts, response type control (JSON, bytes, stream), and a typed DioException with a clear type enum.
Interceptors are why production apps pick Dio. An auth interceptor attaches the bearer token and refreshes it on 401; a logging interceptor records every request for debugging; a retry interceptor retries transient failures. Composing these in one place beats scattering headers['Authorization'] = ... across every call site.
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
},
onError: (e, handler) async {
if (e.response?.statusCode == 401) {
await refreshToken();
return handler.resolve(await dio.fetch(e.requestOptions)));
}
handler.next(e);
},
));
With http, you reimplement this as a wrapper function and call it everywhere, or wrap every call manually.
Dio's CancelToken lets you cancel in-flight requests (e.g. when a user navigates away from a search screen). http has no cancellation primitive — you rely on the underlying HttpClient or ignore the result.
For typeahead/search UIs where stale responses must be discarded, cancellation is essential. We use it across every search-driven screen.
Dio's FormData handles multipart uploads cleanly, including progress callbacks. http supports MultipartRequest but with less ergonomic progress reporting. For apps that upload images/documents (most consumer apps), Dio is noticeably smoother.
If your app makes five GETs to a public API with no auth, no uploads, and no cancellation needs, http is simpler and has zero extra dependencies. Do not reach for Dio for a prototype weather widget.
In production, you need to see what your app is sending and receiving — for debugging user-reported issues and for performance monitoring. Dio's LogInterceptor can log full request/response bodies, headers, and timing, and you can gate verbosity by environment (verbose in dev, redacted in prod). Sentry's Dio integration automatically captures failed HTTP requests as breadcrumbs, so a crash report shows the exact API call that preceded it. With http, you build your own logging wrapper and there is no first-party Sentry breadcrumb integration — you manually capture the request context. For a 5M-user app where triaging a bug means correlating a crash with the API response that triggered it, Dio's observability story is a real operational advantage.
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
dio.interceptors.add(AuthInterceptor(refreshToken));
// every request gets auth, retries on 401, cancels on navigate-away
final r = await dio.get('/me', cancelToken: cancelToken);
// you must attach headers and handle refresh at every call site
final r = await http.get(
Uri.parse('https://api.example.com/me'),
headers: {'Authorization': 'Bearer $token'},
);
if (r.statusCode == 401) {
await refreshToken();
// ... retry manually
}
// no cancellation, no interceptor reuse
Dio centralizes cross-cutting concerns; http pushes them to every call site. For an app with auth, uploads, and cancellation needs, Dio is the clear production choice.
Choose Dio when: the app has auth/token refresh, file uploads, request cancellation, retry policies, or any cross-cutting networking concern. Interceptors make the networking layer maintainable. The default for real apps.
Choose http when: you are writing a script, a tiny app with a few unauthenticated GETs, or you want zero extra dependencies. Simpler is better when the complexity is not there.
Marginally in package size, negligible at runtime. The interceptor model has trivial overhead per request. For any app where networking is a first-class concern, the productivity gain vastly outweighs the footprint.
Not natively. You wrap calls in a helper function that does the auth/logging/retry. It works but scatters logic and is harder to compose than Dio's interceptor chain. At a certain complexity, migrating to Dio is cleaner.
Yes — retrofit (the Dart package) generates type-safe API clients on top of Dio. That gives you typed API interfaces with all of Dio's interceptor/cancellation benefits. A great combo for a large API surface.
Use Dio's built-in MockAdapter or inject a mock Dio instance into your repository. Because Dio is a concrete class with a clean interface, you mock it at the Dio level — set up when(dio.get('/path')) responses and assert on the calls. With http, you typically mock the http.Client interface, which works but means your test doubles model a lower-level abstraction. The practical difference: Dio's interceptor-aware mock lets you test the full request pipeline (including auth header injection) in a unit test, while mocking http.Client tests only the final request/response and leaves interceptor-equivalent logic untested.
Available for hire. Abdelrahman Saed is a Senior Mobile Engineer (Flutter) — open to full-time, fractional, contract, or advisory work. Hire me →
Book a 20-minute call · Download the CV (PDF) · See how I work