countrify 2.5.0 copy "countrify: ^2.5.0" to clipboard
countrify: ^2.5.0 copied to clipboard

A beautiful, customizable country picker for Flutter with 250 countries, 5,300+ states, 150,000+ cities, 5 display modes, theming, and 132 translations.

example/lib/main.dart

import 'package:countrify/countrify.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(const CountrifyExampleApp());

// ═══════════════════════════════════════════════════════════════════════════
// Design Tokens
// ═══════════════════════════════════════════════════════════════════════════

class _C {
  _C._();
  // Primary palette
  static const indigo = Color(0xFF4F46E5);
  static const indigoSubtle = Color(0xFFEEF2FF);

  // Accent
  static const teal = Color(0xFF0D9488);
  static const tealSubtle = Color(0xFFCCFBF1);

  // Neutrals
  static const slate900 = Color(0xFF0F172A);
  static const slate700 = Color(0xFF334155);
  static const slate500 = Color(0xFF64748B);
  static const slate400 = Color(0xFF94A3B8);
  static const slate200 = Color(0xFFE2E8F0);
  static const slate100 = Color(0xFFF1F5F9);
  static const slate50 = Color(0xFFF8FAFC);
  static const white = Colors.white;

  // Surfaces
  static const cardBg = Colors.white;
  static const scaffoldBg = Color(0xFFF8FAFC);
}

// ═══════════════════════════════════════════════════════════════════════════
// App Root
// ═══════════════════════════════════════════════════════════════════════════

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Countrify',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: _C.indigo,
        useMaterial3: true,
        scaffoldBackgroundColor: _C.scaffoldBg,
        appBarTheme: const AppBarTheme(
          systemOverlayStyle: SystemUiOverlayStyle.dark,
        ),
      ),
      home: const HomePage(),
    );
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// Home — Tab Navigation
// ═══════════════════════════════════════════════════════════════════════════

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
  late final TabController _tabController;

  static const _tabs = [
    _TabItem(icon: Icons.phone_outlined, label: 'Phone'),
    _TabItem(icon: Icons.public_outlined, label: 'Country'),
    _TabItem(icon: Icons.location_city_outlined, label: 'Address'),
    _TabItem(icon: Icons.palette_outlined, label: 'Themes'),
    _TabItem(icon: Icons.translate_outlined, label: 'i18n'),
    _TabItem(icon: Icons.widgets_outlined, label: 'Blocks'),
  ];

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabs.length, vsync: this);
    _tabController.addListener(() => setState(() {}));
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => FocusScope.of(context).unfocus(),
      child: Scaffold(
      backgroundColor: _C.scaffoldBg,
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) => [
          SliverAppBar(
            expandedHeight: 120,
            floating: true,
            pinned: true,
            elevation: 0,
            scrolledUnderElevation: 0.5,
            backgroundColor: _C.white,
            surfaceTintColor: Colors.transparent,
            flexibleSpace: FlexibleSpaceBar(
              titlePadding:
                  const EdgeInsets.only(left: 24, bottom: 56),
              title: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Container(
                    width: 28,
                    height: 28,
                    decoration: BoxDecoration(
                      gradient: const LinearGradient(
                        colors: [_C.indigo, _C.teal],
                        begin: Alignment.topLeft,
                        end: Alignment.bottomRight,
                      ),
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: const Icon(
                      Icons.public,
                      color: Colors.white,
                      size: 16,
                    ),
                  ),
                  const SizedBox(width: 10),
                  const Text(
                    'Countrify',
                    style: TextStyle(
                      color: _C.slate900,
                      fontWeight: FontWeight.w700,
                      fontSize: 20,
                      letterSpacing: -0.5,
                    ),
                  ),
                ],
              ),
            ),
            bottom: PreferredSize(
              preferredSize: const Size.fromHeight(52),
              child: Container(
                decoration: const BoxDecoration(
                  color: _C.white,
                  border: Border(
                    bottom: BorderSide(color: _C.slate200, width: 1),
                  ),
                ),
                child: TabBar(
                  controller: _tabController,
                  isScrollable: true,
                  tabAlignment: TabAlignment.start,
                  padding: const EdgeInsets.only(left: 16),
                  labelPadding:
                      const EdgeInsets.symmetric(horizontal: 12),
                  indicatorSize: TabBarIndicatorSize.label,
                  indicatorWeight: 2.5,
                  indicatorColor: _C.indigo,
                  labelColor: _C.indigo,
                  unselectedLabelColor: _C.slate400,
                  labelStyle: const TextStyle(
                    fontSize: 13,
                    fontWeight: FontWeight.w600,
                    letterSpacing: 0.1,
                  ),
                  unselectedLabelStyle: const TextStyle(
                    fontSize: 13,
                    fontWeight: FontWeight.w500,
                  ),
                  splashFactory: NoSplash.splashFactory,
                  tabs: _tabs
                      .map((t) => Tab(
                            height: 44,
                            child: Row(
                              mainAxisSize: MainAxisSize.min,
                              children: [
                                Icon(t.icon, size: 16),
                                const SizedBox(width: 6),
                                Text(t.label),
                              ],
                            ),
                          ))
                      .toList(),
                ),
              ),
            ),
          ),
        ],
        body: TabBarView(
          controller: _tabController,
          children: const [
            PhoneInputTab(),
            CountryPickerTab(),
            AddressTab(),
            ThemingTab(),
            LocalizationTab(),
            BuildingBlocksTab(),
          ],
        ),
      ),
    ),
    );
  }
}

class _TabItem {
  const _TabItem({required this.icon, required this.label});
  final IconData icon;
  final String label;
}

// ═══════════════════════════════════════════════════════════════════════════
// Shared UI Components
// ═══════════════════════════════════════════════════════════════════════════

class _SectionHeader extends StatelessWidget {
  const _SectionHeader({
    required this.title,
    this.subtitle,
    this.icon,
  });
  final String title;
  final String? subtitle;
  final IconData? icon;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if (icon != null)
            Container(
              padding: const EdgeInsets.all(8),
              margin: const EdgeInsets.only(bottom: 12),
              decoration: BoxDecoration(
                color: _C.indigoSubtle,
                borderRadius: BorderRadius.circular(10),
              ),
              child: Icon(icon, size: 20, color: _C.indigo),
            ),
          Text(
            title,
            style: const TextStyle(
              fontSize: 20,
              fontWeight: FontWeight.w700,
              color: _C.slate900,
              letterSpacing: -0.4,
              height: 1.2,
            ),
          ),
          if (subtitle != null) ...[
            const SizedBox(height: 4),
            Text(
              subtitle!,
              style: const TextStyle(
                fontSize: 14,
                color: _C.slate500,
                height: 1.4,
              ),
            ),
          ],
        ],
      ),
    );
  }
}

class _ShowcaseCard extends StatelessWidget {
  const _ShowcaseCard({
    required this.child,
    this.padding,
  });
  final Widget child;
  final EdgeInsetsGeometry? padding;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: padding ?? const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: _C.cardBg,
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: _C.slate200.withValues(alpha: 0.8)),
        boxShadow: [
          BoxShadow(
            color: _C.slate900.withValues(alpha: 0.03),
            blurRadius: 10,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: child,
    );
  }
}

class _InfoRow extends StatelessWidget {
  const _InfoRow({
    required this.label,
    required this.value,
    this.icon,
  });
  final String label;
  final String value;
  final IconData? icon;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        if (icon != null) ...[
          Icon(icon, size: 16, color: _C.slate400),
          const SizedBox(width: 8),
        ],
        Text(
          label,
          style: const TextStyle(
            fontSize: 13,
            color: _C.slate500,
            fontWeight: FontWeight.w500,
          ),
        ),
        const SizedBox(width: 8),
        Flexible(
          child: Text(
            value,
            style: const TextStyle(
              fontSize: 14,
              color: _C.slate900,
              fontWeight: FontWeight.w600,
            ),
            overflow: TextOverflow.ellipsis,
          ),
        ),
      ],
    );
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// Tab 1: Phone Input
// ═══════════════════════════════════════════════════════════════════════════

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

  @override
  State<PhoneInputTab> createState() => _PhoneInputTabState();
}

class _PhoneInputTabState extends State<PhoneInputTab> {
  String _phoneNumber = '';
  String _countryName = 'United States';
  String _dialCode = '+1';
  String _countryEmoji = '';

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.fromLTRB(20, 28, 20, 40),
      children: [
        const _SectionHeader(
          icon: Icons.phone_outlined,
          title: 'Phone Number Input',
          subtitle:
              'A complete phone input with country code picker, validation, and formatting.',
        ),
        _ShowcaseCard(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              PhoneNumberField(
                initialCountryCode: CountryCode.us,
                style: CountrifyFieldStyle.defaultStyle().copyWith(
                  fillColor: _C.slate50,
                  focusedFillColor: _C.indigoSubtle,
                  hintText: 'Enter phone number',
                  cursorColor: _C.indigo,
                  focusedBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                    borderSide:
                        const BorderSide(color: _C.indigo, width: 1.5),
                  ),
                  enabledBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                    borderSide:
                        const BorderSide(color: _C.slate200, width: 1),
                  ),
                ),
                onChanged: (phoneNumber, country) {
                  setState(() {
                    _phoneNumber = phoneNumber;
                    _countryName = country.name;
                    _dialCode = '+${country.callingCodes.first}';
                    _countryEmoji = country.flagEmoji;
                  });
                },
              ),
              const SizedBox(height: 20),
              Container(
                padding: const EdgeInsets.all(16),
                decoration: BoxDecoration(
                  color: _C.slate50,
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Row(
                      children: [
                        Icon(Icons.info_outline,
                            size: 14, color: _C.slate400),
                        SizedBox(width: 6),
                        Text(
                          'Current Value',
                          style: TextStyle(
                            fontSize: 11,
                            fontWeight: FontWeight.w600,
                            color: _C.slate400,
                            letterSpacing: 0.8,
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 12),
                    _InfoRow(
                      icon: Icons.flag_outlined,
                      label: 'Country',
                      value: '$_countryEmoji $_countryName',
                    ),
                    const SizedBox(height: 8),
                    _InfoRow(
                      icon: Icons.dialpad,
                      label: 'Number',
                      value: _phoneNumber.isEmpty
                          ? _dialCode
                          : '$_dialCode $_phoneNumber',
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
        const SizedBox(height: 28),
        const _SectionHeader(
          title: 'Field Styles',
          subtitle: 'Pre-built style variants for different design needs.',
        ),
        _ShowcaseCard(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              _buildStyleLabel('Filled Style'),
              const SizedBox(height: 8),
              PhoneNumberField(
                initialCountryCode: CountryCode.gb,
                style: CountrifyFieldStyle.filledStyle(
                  fillColor: _C.slate100,
                  focusedBorderColor: _C.teal,
                ).copyWith(
                  hintText: 'Filled variant',
                  cursorColor: _C.teal,
                ),
                onChanged: (_, __) {},
              ),
              const SizedBox(height: 20),
              _buildStyleLabel('Outline Style'),
              const SizedBox(height: 8),
              PhoneNumberField(
                initialCountryCode: CountryCode.jp,
                style: CountrifyFieldStyle.outlineStyle(
                  borderColor: _C.slate200,
                  focusedBorderColor: _C.indigo,
                ).copyWith(
                  hintText: 'Outline variant',
                  cursorColor: _C.indigo,
                ),
                onChanged: (_, __) {},
              ),
              const SizedBox(height: 20),
              _buildStyleLabel('Dark Style'),
              const SizedBox(height: 8),
              Container(
                padding: const EdgeInsets.all(16),
                decoration: BoxDecoration(
                  color: const Color(0xFF1A1A2E),
                  borderRadius: BorderRadius.circular(14),
                ),
                child: PhoneNumberField(
                  initialCountryCode: CountryCode.br,
                  style: CountrifyFieldStyle.darkStyle().copyWith(
                    hintText: 'Dark variant',
                  ),
                  onChanged: (_, __) {},
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }

  Widget _buildStyleLabel(String text) {
    return Row(
      children: [
        Container(
          width: 6,
          height: 6,
          decoration: BoxDecoration(
            color: _C.teal,
            borderRadius: BorderRadius.circular(3),
          ),
        ),
        const SizedBox(width: 8),
        Text(
          text,
          style: const TextStyle(
            fontSize: 12,
            fontWeight: FontWeight.w600,
            color: _C.slate500,
            letterSpacing: 0.5,
          ),
        ),
      ],
    );
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// Tab 2: Country Picker
// ═══════════════════════════════════════════════════════════════════════════

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

  @override
  State<CountryPickerTab> createState() => _CountryPickerTabState();
}

class _CountryPickerTabState extends State<CountryPickerTab> {
  String _selectedCountry = 'None';
  String _selectedFlag = '';
  CountryPickerMode _pickerMode = CountryPickerMode.bottomSheet;

  static const _modes = {
    CountryPickerMode.dropdown: ('Dropdown', Icons.arrow_drop_down_circle_outlined),
    CountryPickerMode.bottomSheet: ('Sheet', Icons.drag_handle_rounded),
    CountryPickerMode.dialog: ('Dialog', Icons.picture_in_picture_outlined),
    CountryPickerMode.fullScreen: ('Full', Icons.fullscreen_outlined),
  };

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.fromLTRB(20, 28, 20, 40),
      children: [
        const _SectionHeader(
          icon: Icons.public_outlined,
          title: 'Country Picker',
          subtitle:
              'Choose from four display modes. Tap a mode below to switch.',
        ),
        // Mode selector
        _ShowcaseCard(
          padding: const EdgeInsets.all(12),
          child: Row(
            children: _modes.entries.map((e) {
              final isSelected = _pickerMode == e.key;
              return Expanded(
                child: Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 3),
                  child: GestureDetector(
                    onTap: () =>
                        setState(() => _pickerMode = e.key),
                    child: AnimatedContainer(
                      duration: const Duration(milliseconds: 200),
                      padding: const EdgeInsets.symmetric(vertical: 12),
                      decoration: BoxDecoration(
                        color:
                            isSelected ? _C.indigo : Colors.transparent,
                        borderRadius: BorderRadius.circular(10),
                      ),
                      child: Column(
                        children: [
                          Icon(
                            e.value.$2,
                            size: 20,
                            color: isSelected
                                ? _C.white
                                : _C.slate400,
                          ),
                          const SizedBox(height: 4),
                          Text(
                            e.value.$1,
                            style: TextStyle(
                              fontSize: 11,
                              fontWeight: FontWeight.w600,
                              color: isSelected
                                  ? _C.white
                                  : _C.slate500,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              );
            }).toList(),
          ),
        ),
        const SizedBox(height: 16),
        _ShowcaseCard(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              CountryDropdownField(
                key: ValueKey(_pickerMode),
                pickerMode: _pickerMode,
                style: CountrifyFieldStyle.defaultStyle().copyWith(
                  labelText: 'Select a country',
                  fillColor: _C.slate50,
                  focusedFillColor: _C.indigoSubtle,
                  cursorColor: _C.indigo,
                  enabledBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                    borderSide: const BorderSide(color: _C.slate200),
                  ),
                  focusedBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                    borderSide:
                        const BorderSide(color: _C.indigo, width: 1.5),
                  ),
                ),
                onChanged: (country) {
                  setState(() {
                    _selectedCountry = country.name;
                    _selectedFlag = country.flagEmoji;
                  });
                },
              ),
              if (_selectedCountry != 'None') ...[
                const SizedBox(height: 16),
                Container(
                  padding: const EdgeInsets.symmetric(
                      horizontal: 14, vertical: 10),
                  decoration: BoxDecoration(
                    color: _C.tealSubtle,
                    borderRadius: BorderRadius.circular(10),
                  ),
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      const Icon(Icons.check_circle,
                          size: 16, color: _C.teal),
                      const SizedBox(width: 8),
                      Text(
                        '$_selectedFlag  $_selectedCountry',
                        style: const TextStyle(
                          fontSize: 14,
                          fontWeight: FontWeight.w600,
                          color: _C.teal,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ],
          ),
        ),
      ],
    );
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// Tab 3: Address (Country → State → City)
// ═══════════════════════════════════════════════════════════════════════════

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

  @override
  State<AddressTab> createState() => _AddressTabState();
}

class _AddressTabState extends State<AddressTab> {
  CountryStateCitySelection? _selection;

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.fromLTRB(20, 28, 20, 40),
      children: [
        const _SectionHeader(
          icon: Icons.location_city_outlined,
          title: 'Cascading Address',
          subtitle:
              'Country, state, and city pickers that cascade automatically.',
        ),
        _ShowcaseCard(
          child: CountryStateCityField(
            initialCountryCode: CountryCode.us,
            spacing: 14,
            fieldStyle: CountrifyFieldStyle.defaultStyle().copyWith(
              fillColor: _C.slate50,
              focusedFillColor: _C.indigoSubtle,
              cursorColor: _C.indigo,
              enabledBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
                borderSide: const BorderSide(color: _C.slate200),
              ),
              focusedBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
                borderSide:
                    const BorderSide(color: _C.indigo, width: 1.5),
              ),
            ),
            onChanged: (sel) => setState(() => _selection = sel),
          ),
        ),
        if (_selection != null) ...[
          const SizedBox(height: 16),
          _ShowcaseCard(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Row(
                  children: [
                    Icon(Icons.pin_drop_outlined,
                        size: 14, color: _C.slate400),
                    SizedBox(width: 6),
                    Text(
                      'Selection',
                      style: TextStyle(
                        fontSize: 11,
                        fontWeight: FontWeight.w600,
                        color: _C.slate400,
                        letterSpacing: 0.8,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                _buildSelectionRow(
                  'Country',
                  _selection!.country?.name,
                  _selection!.country?.flagEmoji,
                ),
                const SizedBox(height: 6),
                _buildSelectionRow(
                    'State', _selection!.state?.name, null),
                const SizedBox(height: 6),
                _buildSelectionRow(
                    'City', _selection!.city?.name, null),
                if (_selection!.isComplete) ...[
                  const SizedBox(height: 14),
                  Container(
                    padding: const EdgeInsets.symmetric(
                        horizontal: 12, vertical: 8),
                    decoration: BoxDecoration(
                      color: _C.tealSubtle,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: const Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Icon(Icons.check_circle,
                            size: 14, color: _C.teal),
                        SizedBox(width: 6),
                        Text(
                          'Address complete',
                          style: TextStyle(
                            fontSize: 12,
                            fontWeight: FontWeight.w600,
                            color: _C.teal,
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ],
            ),
          ),
        ],
      ],
    );
  }

  Widget _buildSelectionRow(
      String label, String? value, String? flag) {
    return Row(
      children: [
        SizedBox(
          width: 60,
          child: Text(
            label,
            style: const TextStyle(
              fontSize: 13,
              color: _C.slate400,
              fontWeight: FontWeight.w500,
            ),
          ),
        ),
        if (flag != null) ...[
          Text(flag, style: const TextStyle(fontSize: 16)),
          const SizedBox(width: 6),
        ],
        Flexible(
          child: Text(
            value ?? '--',
            style: TextStyle(
              fontSize: 14,
              fontWeight: FontWeight.w600,
              color: value != null ? _C.slate900 : _C.slate400,
            ),
            overflow: TextOverflow.ellipsis,
          ),
        ),
      ],
    );
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// Tab 4: Theming
// ═══════════════════════════════════════════════════════════════════════════

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

  @override
  State<ThemingTab> createState() => _ThemingTabState();
}

class _ThemingTabState extends State<ThemingTab> {
  int _selectedTheme = 0;

  static const _themes = [
    _ThemeOption(
      label: 'Default',
      icon: Icons.light_mode_outlined,
      desc: 'Clean light theme',
      color: Color(0xFF2196F3),
      bgColor: Colors.white,
    ),
    _ThemeOption(
      label: 'Dark',
      icon: Icons.dark_mode_outlined,
      desc: 'For dark interfaces',
      color: Color(0xFF64B5F6),
      bgColor: Color(0xFF1E1E1E),
    ),
    _ThemeOption(
      label: 'Material 3',
      icon: Icons.auto_awesome_outlined,
      desc: 'Google M3 tokens',
      color: Color(0xFF6750A4),
      bgColor: Color(0xFFFFFBFE),
    ),
    _ThemeOption(
      label: 'Custom',
      icon: Icons.tune_outlined,
      desc: 'Teal accent',
      color: _C.teal,
      bgColor: Colors.white,
    ),
  ];

  CountryPickerTheme get _currentTheme {
    switch (_selectedTheme) {
      case 1:
        return CountryPickerTheme.darkTheme();
      case 2:
        return CountryPickerTheme.material3Theme();
      case 3:
        return CountryPickerTheme.custom(primaryColor: _C.teal);
      default:
        return CountryPickerTheme.defaultTheme();
    }
  }

  CountrifyFieldStyle get _currentFieldStyle {
    if (_selectedTheme == 1) {
      return CountrifyFieldStyle.darkStyle()
          .copyWith(hintText: 'Themed phone field');
    }
    return CountrifyFieldStyle.defaultStyle().copyWith(
      hintText: 'Themed phone field',
      fillColor: _themes[_selectedTheme].bgColor,
      focusedFillColor: _themes[_selectedTheme]
          .color
          .withValues(alpha: 0.08),
      cursorColor: _themes[_selectedTheme].color,
      focusedBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12),
        borderSide: BorderSide(
            color: _themes[_selectedTheme].color, width: 1.5),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final theme = _themes[_selectedTheme];

    return ListView(
      padding: const EdgeInsets.fromLTRB(20, 28, 20, 40),
      children: [
        const _SectionHeader(
          icon: Icons.palette_outlined,
          title: 'Theme Gallery',
          subtitle:
              'Pre-built themes and custom theme support. Tap to preview.',
        ),
        // Theme cards grid
        GridView.count(
          crossAxisCount: 2,
          mainAxisSpacing: 10,
          crossAxisSpacing: 10,
          childAspectRatio: 1.55,
          shrinkWrap: true,
          physics: const NeverScrollableScrollPhysics(),
          children: List.generate(_themes.length, (i) {
            final t = _themes[i];
            final isSelected = _selectedTheme == i;
            return GestureDetector(
              onTap: () => setState(() => _selectedTheme = i),
              child: AnimatedContainer(
                duration: const Duration(milliseconds: 200),
                padding: const EdgeInsets.all(14),
                decoration: BoxDecoration(
                  color: _C.white,
                  borderRadius: BorderRadius.circular(14),
                  border: Border.all(
                    color: isSelected ? t.color : _C.slate200,
                    width: isSelected ? 2 : 1,
                  ),
                  boxShadow: isSelected
                      ? [
                          BoxShadow(
                            color: t.color.withValues(alpha: 0.15),
                            blurRadius: 12,
                            offset: const Offset(0, 4),
                          ),
                        ]
                      : null,
                ),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Container(
                          padding: const EdgeInsets.all(6),
                          decoration: BoxDecoration(
                            color: t.color.withValues(alpha: 0.1),
                            borderRadius: BorderRadius.circular(8),
                          ),
                          child: Icon(t.icon,
                              size: 16, color: t.color),
                        ),
                        const Spacer(),
                        if (isSelected)
                          Container(
                            width: 20,
                            height: 20,
                            decoration: BoxDecoration(
                              color: t.color,
                              shape: BoxShape.circle,
                            ),
                            child: const Icon(Icons.check,
                                size: 12, color: Colors.white),
                          ),
                      ],
                    ),
                    const Spacer(),
                    Text(
                      t.label,
                      style: const TextStyle(
                        fontSize: 14,
                        fontWeight: FontWeight.w700,
                        color: _C.slate900,
                      ),
                    ),
                    Text(
                      t.desc,
                      style: const TextStyle(
                        fontSize: 11,
                        color: _C.slate400,
                      ),
                    ),
                  ],
                ),
              ),
            );
          }),
        ),
        const SizedBox(height: 20),
        // Preview
        AnimatedContainer(
          duration: const Duration(milliseconds: 300),
          padding: const EdgeInsets.all(20),
          decoration: BoxDecoration(
            color: _selectedTheme == 1
                ? const Color(0xFF1A1A2E)
                : _C.white,
            borderRadius: BorderRadius.circular(16),
            border: Border.all(
              color: _selectedTheme == 1
                  ? const Color(0xFF2D2D4A)
                  : _C.slate200.withValues(alpha: 0.8),
            ),
            boxShadow: [
              BoxShadow(
                color: theme.color.withValues(alpha: 0.06),
                blurRadius: 16,
                offset: const Offset(0, 4),
              ),
            ],
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  Container(
                    width: 8,
                    height: 8,
                    decoration: BoxDecoration(
                      color: theme.color,
                      shape: BoxShape.circle,
                    ),
                  ),
                  const SizedBox(width: 8),
                  Text(
                    'Preview',
                    style: TextStyle(
                      fontSize: 12,
                      fontWeight: FontWeight.w600,
                      color: _selectedTheme == 1
                          ? Colors.white60
                          : _C.slate400,
                      letterSpacing: 0.6,
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 16),
              PhoneNumberField(
                key: ValueKey(_selectedTheme),
                initialCountryCode: CountryCode.us,
                theme: _currentTheme,
                style: _currentFieldStyle,
                onChanged: (phone, country) {},
              ),
            ],
          ),
        ),
      ],
    );
  }
}

class _ThemeOption {
  const _ThemeOption({
    required this.label,
    required this.icon,
    required this.desc,
    required this.color,
    required this.bgColor,
  });
  final String label;
  final IconData icon;
  final String desc;
  final Color color;
  final Color bgColor;
}

// ═══════════════════════════════════════════════════════════════════════════
// Tab 5: Localization
// ═══════════════════════════════════════════════════════════════════════════

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

  @override
  State<LocalizationTab> createState() => _LocalizationTabState();
}

class _LocalizationTabState extends State<LocalizationTab> {
  String _selectedLocale = 'en';

  static const _locales = <String, (String, String)>{
    'en': ('English', 'EN'),
    'ar': ('Arabic', 'AR'),
    'fr': ('French', 'FR'),
    'es': ('Spanish', 'ES'),
    'de': ('German', 'DE'),
    'zh': ('Chinese', 'ZH'),
    'ja': ('Japanese', 'JA'),
  };

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.fromLTRB(20, 28, 20, 0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const _SectionHeader(
                icon: Icons.translate_outlined,
                title: 'Localization',
                subtitle:
                    'Country names translated into 7 languages.',
              ),
              // Language chips
              SizedBox(
                height: 42,
                child: ListView.separated(
                  scrollDirection: Axis.horizontal,
                  itemCount: _locales.length,
                  separatorBuilder: (_, __) =>
                      const SizedBox(width: 8),
                  itemBuilder: (_, i) {
                    final entry =
                        _locales.entries.elementAt(i);
                    final isSelected =
                        _selectedLocale == entry.key;
                    return GestureDetector(
                      onTap: () => setState(
                          () => _selectedLocale = entry.key),
                      child: AnimatedContainer(
                        duration:
                            const Duration(milliseconds: 200),
                        padding: const EdgeInsets.symmetric(
                            horizontal: 14),
                        decoration: BoxDecoration(
                          color: isSelected
                              ? _C.indigo
                              : _C.white,
                          borderRadius:
                              BorderRadius.circular(10),
                          border: Border.all(
                            color: isSelected
                                ? _C.indigo
                                : _C.slate200,
                          ),
                          boxShadow: isSelected
                              ? [
                                  BoxShadow(
                                    color: _C.indigo
                                        .withValues(alpha: 0.2),
                                    blurRadius: 6,
                                    offset:
                                        const Offset(0, 2),
                                  ),
                                ]
                              : null,
                        ),
                        alignment: Alignment.center,
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            Text(
                              entry.value.$2,
                              style: TextStyle(
                                fontSize: 11,
                                fontWeight: FontWeight.w700,
                                color: isSelected
                                    ? _C.white
                                        .withValues(alpha: 0.7)
                                    : _C.slate400,
                                letterSpacing: 0.5,
                              ),
                            ),
                            const SizedBox(width: 6),
                            Text(
                              entry.value.$1,
                              style: TextStyle(
                                fontSize: 13,
                                fontWeight: FontWeight.w600,
                                color: isSelected
                                    ? _C.white
                                    : _C.slate700,
                              ),
                            ),
                          ],
                        ),
                      ),
                    );
                  },
                ),
              ),
            ],
          ),
        ),
        const SizedBox(height: 16),
        Expanded(
          child: Padding(
            padding: const EdgeInsets.fromLTRB(20, 0, 20, 8),
            child: Container(
              clipBehavior: Clip.antiAlias,
              decoration: BoxDecoration(
                color: _C.cardBg,
                borderRadius: BorderRadius.circular(16),
                border: Border.all(
                    color: _C.slate200.withValues(alpha: 0.8)),
                boxShadow: [
                  BoxShadow(
                    color: _C.slate900.withValues(alpha: 0.03),
                    blurRadius: 10,
                    offset: const Offset(0, 2),
                  ),
                ],
              ),
              child: CountryPicker(
                key: ValueKey(_selectedLocale),
                pickerType: CountryPickerType.inline,
                config: CountryPickerConfig(
                  locale: _selectedLocale,
                ),
                onCountrySelected: (country) {
                  debugPrint('Selected: ${country.name}');
                },
              ),
            ),
          ),
        ),
      ],
    );
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// Tab 6: Building Blocks
// ═══════════════════════════════════════════════════════════════════════════

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

  @override
  State<BuildingBlocksTab> createState() => _BuildingBlocksTabState();
}

class _BuildingBlocksTabState extends State<BuildingBlocksTab> {
  late Country _selectedCountry;

  @override
  void initState() {
    super.initState();
    _selectedCountry = CountryUtils.getCountryByCode(CountryCode.us)!;
  }

  @override
  Widget build(BuildContext context) {
    final us = CountryUtils.getCountryByCode(CountryCode.us)!;
    final gb = CountryUtils.getCountryByCode(CountryCode.gb)!;
    final jp = CountryUtils.getCountryByCode(CountryCode.jp)!;
    final br = CountryUtils.getCountryByCode(CountryCode.br)!;
    final de = CountryUtils.getCountryByCode(CountryCode.de)!;
    final fr = CountryUtils.getCountryByCode(CountryCode.fr)!;

    return ListView(
      padding: const EdgeInsets.fromLTRB(20, 28, 20, 40),
      children: [
        const _SectionHeader(
          icon: Icons.widgets_outlined,
          title: 'Building Blocks',
          subtitle:
              'Individual components you can use independently.',
        ),

        // Flags
        _buildBlockLabel('CountryFlag'),
        const SizedBox(height: 10),
        _ShowcaseCard(
          child: Wrap(
            spacing: 20,
            runSpacing: 12,
            children: [us, gb, jp, br, de, fr].map((c) {
              return Column(
                children: [
                  Container(
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(6),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black.withValues(alpha: 0.08),
                          blurRadius: 4,
                          offset: const Offset(0, 1),
                        ),
                      ],
                    ),
                    child: ClipRRect(
                      borderRadius: BorderRadius.circular(4),
                      child: CountryFlag(
                        country: c,
                        size: const Size(48, 34),
                      ),
                    ),
                  ),
                  const SizedBox(height: 6),
                  Text(
                    c.alpha2Code,
                    style: const TextStyle(
                      fontSize: 11,
                      fontWeight: FontWeight.w600,
                      color: _C.slate400,
                      letterSpacing: 0.5,
                    ),
                  ),
                ],
              );
            }).toList(),
          ),
        ),

        const SizedBox(height: 28),

        // Search bar
        _buildBlockLabel('CountrySearchBar'),
        const SizedBox(height: 10),
        _ShowcaseCard(
          child: CountrySearchBar(
            hintText: 'Search countries...',
            onChanged: (query) => debugPrint('Search: $query'),
          ),
        ),

        const SizedBox(height: 28),

        // List tiles
        _buildBlockLabel('CountryListTile'),
        const SizedBox(height: 10),
        _ShowcaseCard(
          padding: const EdgeInsets.symmetric(
              horizontal: 8, vertical: 8),
          child: Column(
            children: [us, gb, jp, br].map((c) {
              return CountryListTile(
                country: c,
                isSelected:
                    _selectedCountry.alpha2Code == c.alpha2Code,
                onTap: (country) =>
                    setState(() => _selectedCountry = country),
              );
            }).toList(),
          ),
        ),

        const SizedBox(height: 28),

        // Phone Code Picker
        _buildBlockLabel('PhoneCodePicker'),
        const SizedBox(height: 10),
        _ShowcaseCard(
          child: PhoneCodePicker(
            initialCountryCode: CountryCode.us,
            onChanged: (country) {
              debugPrint(
                  'Code: +${country.callingCodes.first}');
            },
          ),
        ),
      ],
    );
  }

  static Widget _buildBlockLabel(String text) {
    return Row(
      children: [
        Container(
          padding:
              const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
          decoration: BoxDecoration(
            color: _C.indigoSubtle,
            borderRadius: BorderRadius.circular(6),
          ),
          child: Text(
            text,
            style: const TextStyle(
              fontSize: 12,
              fontWeight: FontWeight.w700,
              color: _C.indigo,
              letterSpacing: 0.3,
              fontFamily: 'monospace',
            ),
          ),
        ),
      ],
    );
  }
}
35
likes
160
points
1.71k
downloads

Documentation

API reference

Publisher

verified publishergocodeable.com

Weekly Downloads

A beautiful, customizable country picker for Flutter with 250 countries, 5,300+ states, 150,000+ cities, 5 display modes, theming, and 132 translations.

Homepage
Repository (GitHub)
View/report issues

Topics

#country-picker #country #state #city #widget

License

MIT (license)

Dependencies

flutter

More

Packages that depend on countrify