carrier_info_plus
Cellular carrier, SIM and network information for Flutter.
A maintained replacement for the unmaintained carrier_info,
built for Flutter 3.47.5+ with Swift Package Manager support, AGP 9-ready Gradle
config on Flutter's built-in Kotlin, and typed enums instead of stringly-typed
fields.
Important
This is not a drop-in replacement. The _plus suffix usually signals a
compatible fork; this one is a ground-up rewrite with a different API. The old
package advertised a lot of fields that modern Android and iOS no longer
populate, and carrying those forward would have meant shipping a contract we
could not honour. See Migrating below.
What each platform can actually tell you
This is the part most carrier packages gloss over. Apple removed CTCarrier in
iOS 16, so carrier identity is gone there for every app, no matter how well
maintained the plugin is.
| Android | iOS 16+ | |
|---|---|---|
| Carrier name | ✅ | ❌ removed by Apple |
| MCC / MNC | ✅ | ❌ removed by Apple |
| Country ISO | ✅ | ❌ removed by Apple |
| Per-SIM enumeration (dual SIM) | ✅ | ❌ count only |
| Which SIM is default for data / voice | ✅ | ❌ |
| SIM state | ✅ | ❌ |
| Roaming | ✅ | ❌ |
| Radio technology (LTE / 5G NR) | ✅ | ✅ |
| Network generation | ✅ | ✅ |
| eSIM support | ✅ | ✅ |
| SMS / voice capability | ✅ | ✅ |
| Cellular data availability | ✅ | ✅ |
Rather than handing you a struct full of unexplained nulls, every result carries
a support block telling you what was answerable and why not:
final info = await CarrierInfoPlus.get();
if (info.support.carrierIdentityAvailable) {
showCarrier(info.primarySim?.carrierName);
} else if (info.support.limitation.isRecoverable) {
showPermissionPrompt(); // Android, READ_PHONE_STATE not granted
} else {
hideCarrierRow(); // iOS 16+, nothing to show and nothing to ask for
}
That distinction — "the user said no" versus "this OS will never answer" — is the whole reason this package exists. They look identical in the data and mean completely different things in a UI.
Install
flutter pub add carrier_info_plus
No iOS setup required. SwiftPM and CocoaPods are both supported, so it works whether or not your app has migrated.
No Android Gradle setup required either. This package applies no Kotlin Gradle plugin of its own, so it builds whichever way your app is configured:
Your app's android.builtInKotlin |
Who compiles this package's Kotlin |
|---|---|
true |
AGP 9, directly |
false (what flutter create writes today) |
Flutter, by applying KGP for us |
Android permissions
This package declares no permissions of its own. READ_PHONE_STATE is a
runtime permission that appears in the Play Console and obliges you to file a
data-safety declaration, so it's your call, not the package's.
Without any permission you still get: network operator, SIM state, and the active SIM's MCC/MNC and country. For per-SIM data on a dual-SIM device, add:
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
Then request it at runtime — either with your existing permission_handler
setup, or with the built-in helper:
if (!await CarrierInfoPlus.hasPermission()) {
await CarrierInfoPlus.requestPermission();
}
One optional extra: capabilities.isDataEnabled and network.cellularDataState
are read through an API that accepts ACCESS_NETWORK_STATE rather than
READ_PHONE_STATE. That one is install-time and never prompts, so declare it if
you want those two fields:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Usage
import 'package:carrier_info_plus/carrier_info_plus.dart';
final info = await CarrierInfoPlus.get();
// Network — works on both platforms
print(info.generation); // NetworkGeneration.fiveG
print(info.network.radioTechnologies); // [RadioAccessTechnology.nr]
print(info.network.cellularDataState); // CellularDataState.notRestricted
// Device capabilities
print(info.capabilities.isMultiSimSupported); // bool, not a String
print(info.capabilities.supportsEmbeddedSim);
// The SIM your data actually runs over — not merely the first one
print(info.primarySim?.carrierName);
print(info.voiceSim?.carrierName); // often a different SIM
// Per-SIM — Android
for (final sim in info.simCards) {
print('${sim.slotIndex}: ${sim.carrierName} (${sim.plmn})');
print('eSIM: ${sim.isEmbedded}, roaming: ${sim.isRoaming}');
}
// How many SIMs exist, which can exceed how many could be described
print(info.simCount);
get() returns a snapshot, not a live view. Nothing is cached, because SIM
and network state change underneath you — re-read after a SIM swap or when
returning from the background.
It never throws for the ordinary "cannot answer" cases. An unsupported platform,
a missing permission and a device with no radio all come back as a populated
CarrierInfo whose support block explains the gap. A PlatformException
still propagates, because that means something genuinely broke.
simCount versus simCards.length
They answer different questions, and on iOS they differ:
info.simCount // 2 — the platform knows there are two services
info.simCards.length // 0 — it will not describe either of them
Use simCount (or isDualSimActive, which prefers it) when asking how many.
Use simCards when you need to render something about each one.
Migrating from 1.x
The shape is unchanged; four things moved.
| Change | Why |
|---|---|
primarySim is now the SIM flagged isDefaultData, falling back to the first |
1.x returned simCards.first, which names the wrong carrier on a dual-SIM phone running data on slot 2 |
voiceSim, SimCard.isDefaultData, SimCard.isDefaultVoice added |
Voice and data routinely use different SIMs |
CarrierInfo.simCount added; iOS no longer emits placeholder SimCards |
iOS can count services without identifying them. 1.x returned entries with every field null; now the list is empty and the count is real |
RadioAccessTechnology.lteCa removed, nrNsa added |
lteCa had no public constant on either platform and could never be reported. nrNsa is non-standalone 5G, which iOS names and Android reports as lte |
The minimum SDK is now Flutter 3.47.5 / Dart 3.13.4.
Migrating from carrier_info
The platform split is gone. Both getAndroidInfo() and getIosInfo() map to
the same CarrierInfoPlus.get().
carrier_info |
carrier_info_plus |
|---|---|
CarrierInfo.getAndroidInfo() |
CarrierInfoPlus.get() |
CarrierInfo.getIosInfo() |
CarrierInfoPlus.get() |
AndroidCarrierData.subscriptionsInfo |
CarrierInfo.simCards |
AndroidCarrierData.isMultiSimSupported (String) |
capabilities.isMultiSimSupported (bool) |
TelephonyInfo.networkGeneration (String) |
network.generation (NetworkGeneration) |
TelephonyInfo.radioType (String) |
network.radioTechnologies (List<RadioAccessTechnology>) |
TelephonyInfo.simState (String) |
SimCard.state (SimState) |
IosCarrierData.carrierRadioAccessTechnologyTypeList |
network.radioTechnologies |
IosCarrierData.supportsEmbeddedSIM |
capabilities.supportsEmbeddedSim |
IosCarrierData.isSIMInserted |
CarrierInfo.hasSim |
toMap()['_ios_version_info'] |
CarrierInfo.support |
The platform difference hasn't vanished — it moved out of the type and into
the data. iOS returns fewer populated fields, and support says why. You can
still branch on Platform.isIOS, but info.support.carrierIdentityAvailable is
the better condition: it tests the thing you care about rather than a proxy for
it.
Fields with no replacement
Removed because they no longer return data on a current OS:
| Removed | Why |
|---|---|
simSerialNo |
Null since Android 10 for non-privileged apps |
phoneNumber |
Empty on most carriers even with READ_PHONE_NUMBERS |
cellId / lac |
Needs location permission for a GSM-only legacy path |
iOS subscriberIdentifiers, carrierTokens |
Not obtainable on modern iOS |
You can also delete five permissions from your manifest. This package needs
only READ_PHONE_STATE, and only for per-SIM data. In particular drop
READ_PRIVILEGED_PHONE_STATE — it is signature-level, so no Play Store app can
ever hold it, and listing it invites Play Console review questions for nothing.
Why iOS returns so little
Apple deprecated CTCarrier in iOS 16. carrierName returns "--",
mobileCountryCode and mobileNetworkCode return nil, and
serviceSubscriberCellularProviders went with them.
This package deliberately ships no deprecated fallback for older iOS. Two reasons: behaviour shouldn't silently change under your users as they update, and calling deprecated CoreTelephony API is a build break waiting to happen when Apple finally removes it. iOS reports carrier identity as unavailable on every version, consistently.
What remains genuinely readable on iOS is real and useful: radio access technology per active service, how many services there are, eSIM provisioning support, SMS capability, and whether your app may use cellular data.
Development
Run the example app to see every field this package exposes against your own device:
cd example && flutter run
The Android emulator ships a fake T-Mobile SIM (MCC 310, MNC 260) and
reports a carrier, country, SIM state and radio technology, so most development
needs no hardware. It does not emulate dual-SIM, eSIM or roaming, and reports
isMultiSimSupported and supportsEmbeddedSim as false regardless.
The iOS Simulator has no cellular hardware at all, so every field is empty
and limitation is noTelephonyHardware. iOS behaviour has to be checked on a
device.
See CONTRIBUTING.md for the full setup.
Contributing
Issues and pull requests welcome. The package is small on purpose — if you're adding a field, please include what OS versions actually populate it.
License
MIT — see LICENSE.
Libraries
- carrier_info_plus
- Cellular carrier, SIM and network information for Flutter.