skip to content

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?

level: seniorimportance: nice to knowfreq 20%

answer

  1. dart:io sockets versus the platform stack
  2. VPNs, proxies, HTTP/3, caching
  3. everything implements http.Client
  4. one factory chooses per platform
  5. NativeAdapter for dio

basics

~20 s

The 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 lines
dart
import '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

for a junior

Know that the default client is Dart's own and that cupertino_http and cronet_http use the phone's native networking.

for a middle

Explain the VPN, proxy, HTTP/3 and caching benefits and how a platform factory returns an http.Client so call sites do not change.

for a senior

Plan the switch: inject the client, keep fakes for unit tests, handle devices without Play Services, and retest cookies, redirects and caching on devices.

for a principal

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.