Why would a Flutter app swap package:http's default client for cupertino_http or cronet_http, and how do you wire that in without changing call sites?
answer
- dart:io sockets versus the platform stack
- VPNs, proxies, HTTP/3, caching
- everything implements http.Client
- one factory chooses per platform
- NativeAdapter for dio
basics
~20 sThe default IOClient uses dart:io sockets; cupertino_http (URLSession) and cronet_http (Cronet) use the platform stack, gaining VPN and proxy support, HTTP/3 and platform caching. Both implement http.Client, so a factory picks one per platform and call sites stay unchanged.
solid answer
~40 s`Client()` gives an `IOClient` built on `dart:io`'s socket-based `HttpClient`. `cupertino_http` wraps Apple's Foundation URL Loading System and `cronet_http` wraps Cronet on Android; their READMEs list automatic support for platform VPN and proxy settings, HTTP/3, and configurable caching or Wi-Fi-only policies. Both implement `package:http`'s `Client`, so you write one factory: `CronetClient.fromCronetEngine(CronetEngine.build(...))` on Android, `CupertinoClient.fromSessionConfiguration(...)` on iOS and macOS, `IOClient` elsewhere, and inject the result. The http docs advise Flutter apps to pass the client through something like `provider` rather than `runWithClient`, because Flutter does not guarantee callbacks run in a particular zone. dio users get the same stacks with `native_dio_adapter`'s `NativeAdapter`. `cronet_http` depends on Google Play Services unless built with `--dart-define=cronetHttpNoPlay=true`.
code
dart · 24 linesimport 'dart:io';
import 'package:cronet_http/cronet_http.dart';
import 'package:cupertino_http/cupertino_http.dart';
import 'package:http/http.dart';
import 'package:http/io_client.dart';
Client createHttpClient() {
if (Platform.isAndroid) {
final engine = CronetEngine.build(
cacheMode: CacheMode.memory,
cacheMaxSize: 2 * 1024 * 1024,
userAgent: 'GymApp',
);
return CronetClient.fromCronetEngine(engine, closeEngine: true);
}
if (Platform.isIOS || Platform.isMacOS) {
final config = URLSessionConfiguration.ephemeralSessionConfiguration()
..cache = URLCache.withCapacity(memoryCapacity: 2 * 1024 * 1024)
..httpAdditionalHeaders = {'User-Agent': 'GymApp'};
return CupertinoClient.fromSessionConfiguration(config);
}
return IOClient();
}go deeper
Know that the default client is Dart's own and that cupertino_http and cronet_http use the phone's native networking.
Explain the VPN, proxy, HTTP/3 and caching benefits and how a platform factory returns an http.Client so call sites do not change.
Plan the switch: inject the client, keep fakes for unit tests, handle devices without Play Services, and retest cookies, redirects and caching on devices.
Decide whether native stacks are worth the plugin, size and testing cost for the app's users and network environments.
## Three HTTP stacks on a phone A Flutter app on Android or iOS can send HTTP through different engines: | Client | Engine | Platforms | |---|---|---| | `IOClient` (what `Client()` returns off the web) | `dart:io` `HttpClient`, Dart's own socket-based implementation | all native platforms | | `CupertinoClient` from `cupertino_http` | Apple's Foundation URL Loading System (`URLSession`) | iOS, macOS | | `CronetClient` from `cronet_http` | Cronet, Chromium's network stack | Android | The `dart:io` stack is portable and needs no plugin, but it does not know about the operating system's networking configuration. ## What the native stacks add The package READMEs list the reasons to switch: - **Platform networking features**: VPNs and HTTP proxies configured on the device are honoured automatically. - **More configuration**: for example Wi-Fi-only access or blocking cookies on Apple platforms, and configurable caching with Cronet. - **More protocol features**: HTTP/3 and custom redirect handling. For a gym-membership app used on corporate phones behind proxies, or by members on gym Wi-Fi with a captive portal, the platform stack tends to behave the way other apps on the phone behave, which is what support teams expect. ## Wiring it in without touching call sites Because `CupertinoClient` and `CronetClient` implement `package:http`'s `Client`, every repository that already takes a `Client` keeps working: 1. Write a `Client httpClient()` factory that checks the platform. 2. On Android, build a `CronetEngine` (`cacheMode`, `cacheMaxSize`, `userAgent`) and return `CronetClient.fromCronetEngine(engine, closeEngine: true)`. 3. On iOS and macOS, build a `URLSessionConfiguration` (for example `ephemeralSessionConfiguration()` with a `URLCache`) and return `CupertinoClient.fromSessionConfiguration(config)`. 4. Otherwise return `IOClient()` (and on the web the default browser client). 5. Create the client **once** and provide it to the app; close it when the app's root scope is disposed. `package:http` also has `runWithClient`, which makes `Client()` and the top-level functions return your client inside a zone. Its documentation warns that Flutter does not guarantee callbacks run in a particular zone and recommends providing the client through a framework such as `provider` instead. ## dio users dio talks to the network through an `HttpClientAdapter`. The `native_dio_adapter` plugin supplies `NativeAdapter`, which uses `cupertino_http` on iOS and macOS and `cronet_http` on Android, while other platforms keep the Dart stack: `dio.httpClientAdapter = NativeAdapter();`. Interceptors, `BaseOptions` and `CancelToken` keep working on top. ## Costs and traps - **Plugins need a platform.** Native clients cannot run in plain `flutter test` unit tests, so inject the `Client` and use a fake there. - **Google Play Services.** `cronet_http` uses the Cronet provider from Google Play Services by default. Devices without it need the embedded build, selected with `--dart-define=cronetHttpNoPlay=true` (also for `flutter test`). `native_dio_adapter` documents devices whose Cronet providers are all disabled and offers an opt-in `createFallbackAdapter`. - **Different behaviour, same API.** Caching, redirect and cookie behaviour follow the platform stack, so test the flows that depend on them (login cookies, redirects to a CDN) on real devices after switching. ## Migrating step by step 1. Make every repository take an `http.Client` (or a `Dio`) through its constructor, if it does not already. 2. Add the factory and switch one platform at a time, starting with the one where users report proxy or VPN problems. 3. Retest login, cookie-based flows, redirects and uploads on real devices, because they follow the platform stack's rules after the switch. 4. Keep `IOClient` or a fake for unit tests and desktop targets. On the **web** none of this applies: `Client()` returns a `BrowserClient`, which uses the browser's Fetch API since http 1.3, so the browser's own stack is already in use. ## When not to bother A small app talking to one public API without proxy or HTTP/3 requirements gains little. The switch pays off when users report "works in the browser, not in the app" on managed networks, or when platform caching and HTTP/3 are product goals.
- Why do the http docs discourage runWithClient in Flutter apps?`runWithClient` sets the default client through a `Zone`, and Flutter does not guarantee that framework callbacks run in the zone where you called it. Providing the client explicitly, for example with `provider`, makes the dependency visible and reliable.
- Your Android release works on phones with Google services but every request fails on devices without them; what changed?`cronet_http` uses the Cronet provider from Google Play Services by default. Build with `--dart-define=cronetHttpNoPlay=true` to bundle embedded Cronet, or fall back to `IOClient` when Cronet is unavailable.
- How do dio users get the same native stacks?Set `dio.httpClientAdapter = NativeAdapter()` from `native_dio_adapter`. It uses `cupertino_http` on iOS and macOS and `cronet_http` on Android; interceptors, options and cancel tokens are unchanged.
saying these in an interview costs you the question
- Flutter's default http client already uses URLSession on iOS and Cronet on Android.
- Switching to cronet_http requires rewriting every repository.
- Native clients work in plain unit tests like IOClient does.
- runWithClient is the recommended way to inject a client in Flutter.
- cronet_http works on every Android device without extra configuration.