country_subdivision_data 0.0.3 copy "country_subdivision_data: ^0.0.3" to clipboard
country_subdivision_data: ^0.0.3 copied to clipboard

Offline country, subdivision, city, and calling-code data with lazy loading, customizable phone fields, and searchable country pickers.

example/lib/main.dart

import 'dart:async';

import 'package:country_subdivision_data/country_subdivision_data.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const ExampleApp());
}

final class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'Country subdivision data',
        theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
        home: const LocationExplorer(),
      );
}

final class LocationExplorer extends StatefulWidget {
  const LocationExplorer({super.key});

  @override
  State<LocationExplorer> createState() => _LocationExplorerState();
}

final class _LocationExplorerState extends State<LocationExplorer> {
  final CountrySubdivisionData _data = CountrySubdivisionData.instance;
  final TextEditingController _searchController = TextEditingController();
  List<Country> _countries = const <Country>[];
  List<Subdivision> _subdivisions = const <Subdivision>[];
  List<City> _cities = const <City>[];
  List<City> _results = const <City>[];
  Country? _country;
  Subdivision? _subdivision;
  Object? _error;
  bool _loading = true;
  String _phoneNumber = '';

  @override
  void initState() {
    super.initState();
    unawaited(_initialize());
  }

  Future<void> _initialize() async {
    setState(() {
      _loading = true;
      _error = null;
    });
    try {
      await _data.initialize();
      final countries = await _data.getCountries();
      if (mounted) {
        setState(() {
          _countries = countries;
          _loading = false;
        });
      }
    } on Object catch (error) {
      if (mounted) {
        setState(() {
          _error = error;
          _loading = false;
        });
      }
    }
  }

  Future<void> _selectCountry(Country? country) async {
    setState(() {
      _country = country;
      _subdivision = null;
      _subdivisions = const <Subdivision>[];
      _cities = const <City>[];
      _results = const <City>[];
      _loading = country != null;
    });
    if (country == null) {
      return;
    }
    try {
      final subdivisions = await _data.getSubdivisions(
        countryCode: country.iso2,
      );
      if (mounted && _country == country) {
        setState(() {
          _subdivisions = subdivisions;
          _loading = false;
        });
      }
    } on Object catch (error) {
      _showError(error);
    }
  }

  Future<void> _selectSubdivision(Subdivision? subdivision) async {
    setState(() {
      _subdivision = subdivision;
      _cities = const <City>[];
      _results = const <City>[];
      _loading = subdivision != null;
    });
    final country = _country;
    if (country == null || subdivision == null) {
      return;
    }
    try {
      final cities = await _data.getCities(
        countryCode: country.iso2,
        subdivisionCode: subdivision.code,
      );
      if (mounted && _subdivision == subdivision) {
        setState(() {
          _cities = cities;
          _loading = false;
        });
      }
    } on Object catch (error) {
      _showError(error);
    }
  }

  Future<void> _search(String query) async {
    final country = _country;
    if (country == null) {
      return;
    }
    final results = await _data.searchCities(
      query: query,
      countryCode: country.iso2,
      subdivisionCode: _subdivision?.code,
    );
    if (mounted) {
      setState(() {
        _results = results;
      });
    }
  }

  Future<void> _clearAndReload() async {
    await _data.clearCache();
    final country = _country;
    if (country != null) {
      await _selectCountry(country);
    }
  }

  void _showError(Object error) {
    if (mounted) {
      setState(() {
        _error = error;
        _loading = false;
      });
    }
  }

  @override
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_error != null) {
      return Scaffold(
        appBar: AppBar(title: const Text('Location explorer')),
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Text('Could not load locations: $_error'),
              FilledButton(onPressed: _initialize, child: const Text('Retry')),
            ],
          ),
        ),
      );
    }
    return Scaffold(
      appBar: AppBar(title: const Text('Location explorer')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: <Widget>[
          if (_loading) const LinearProgressIndicator(),
          CountryPhoneField(
            initialCountryCode: 'NG',
            favoriteCountryCodes: const <String>['NG', 'US', 'GB'],
            decoration: const InputDecoration(
              labelText: 'Phone number',
              border: OutlineInputBorder(),
            ),
            pickerConfiguration: const CountryPickerDialogConfiguration(
              title: 'Choose a calling code',
              searchHintText: 'Search country, ISO code, or calling code',
              showIsoCode: true,
            ),
            validator: (value) =>
                value == null || value.isEmpty ? 'Enter a phone number' : null,
            onChanged: (value) {
              setState(() {
                _phoneNumber = value.internationalNumber;
              });
            },
          ),
          if (_phoneNumber.isNotEmpty)
            Padding(
              padding: const EdgeInsets.only(top: 8),
              child: Text('Composed number: $_phoneNumber'),
            ),
          const SizedBox(height: 20),
          DropdownButtonFormField<Country>(
            initialValue: _country,
            decoration: const InputDecoration(labelText: 'Country'),
            items: _countries
                .map(
                  (country) => DropdownMenuItem<Country>(
                    value: country,
                    child: Text(country.name),
                  ),
                )
                .toList(growable: false),
            onChanged: _selectCountry,
          ),
          const SizedBox(height: 12),
          DropdownButtonFormField<Subdivision>(
            initialValue: _subdivision,
            decoration: const InputDecoration(labelText: 'Subdivision'),
            items: _subdivisions
                .map(
                  (subdivision) => DropdownMenuItem<Subdivision>(
                    value: subdivision,
                    child: Text(subdivision.name),
                  ),
                )
                .toList(growable: false),
            onChanged: _country == null ? null : _selectSubdivision,
          ),
          const SizedBox(height: 12),
          TextField(
            controller: _searchController,
            enabled: _country != null,
            decoration: const InputDecoration(
              labelText: 'Search cities',
              suffixIcon: Icon(Icons.search),
            ),
            onChanged: _search,
          ),
          const SizedBox(height: 12),
          Text(
            _subdivision == null
                ? 'Select a subdivision to load cities.'
                : _cities.isEmpty
                    ? 'No cities are listed for this subdivision.'
                    : '${_cities.length} cities loaded.',
          ),
          for (final city in _results)
            ListTile(
                title: Text(city.name), subtitle: Text(city.timezone ?? '')),
          TextButton.icon(
            onPressed: () {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(
                  content: Text(
                    'Store a user-entered location separately from the dataset.',
                  ),
                ),
              );
            },
            icon: const Icon(Icons.edit_location_alt),
            label: const Text('Location not listed'),
          ),
          OutlinedButton(
            onPressed: _clearAndReload,
            child: const Text('Clear cache and reload'),
          ),
          const Divider(),
          if (_data.isInitialized)
            Text(
              'Snapshot ${_data.snapshotMetadata.upstreamCommit.substring(0, 12)}\n'
              '${_data.snapshotMetadata.countryCount} countries, '
              '${_data.snapshotMetadata.subdivisionCount} subdivisions, '
              '${_data.snapshotMetadata.cityCount} cities',
            ),
        ],
      ),
    );
  }
}
1
likes
160
points
154
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Offline country, subdivision, city, and calling-code data with lazy loading, customizable phone fields, and searchable country pickers.

Repository (GitHub)
View/report issues
Contributing

Topics

#countries #subdivisions #cities #geodata #iso-codes

License

BSD-3-Clause (license)

Dependencies

archive, crypto, flutter

More

Packages that depend on country_subdivision_data