Skip to content

How can I precache all the flags in circle_flags to make them available offline? #293

@MTaimoor99

Description

@MTaimoor99

I am trying to preload all the SVGs inside this package to make them available offline. To do so, I went through the documentation of the circle_flags package, which introduced me to the preload function as below:

CircleFlag.preload(['fr', 'us']);

I have had a look at this function under the hood, it is using the same methodology as I am to precache my local SVGs and make them available offline. So, I decided to:

  1. Use this function in the build method of country_button.dart first. I moved the example here inside a dialog. Then when the app compiled, I right clicked the screen, clicked the Network tab, and then set the throttling to 'Offline'. Upon clicking the button to show the dialog, I could see that the US flag SVG was not rendering at all, indicating that the preload function had not worked properly.
  2. I decided to create an assets/svgs folder at the same level as the lib folder and moved all the SVGs inside circle_flags to this folder. I then decided to add a class with an initialization function that would precache these local SVGs. However, that approach has not worked either.
  3. I then decided to add a class with an initialization function in lib/src/phone_form_field.dart which was supposed to preload the flags using CircleFlag.preload as below:
library phone_number_input;

export 'src/phone_form_field.dart';
export 'src/country_selector_navigator.dart';
export 'src/country_button.dart';
export 'src/country_button_style.dart';

export 'src/validation/phone_validator.dart';
export 'src/localization/localization.dart';

export 'package:phone_numbers_parser/phone_numbers_parser.dart'
    show PhoneNumber, PhoneNumberType, IsoCode;
import 'package:circle_flags/circle_flags.dart';

/// Initialization class for the phone_form_field package
class PhoneNumberInputPackage {
  static bool _initialized = false;
  
  /// Initialize the package by preloading country flag assets
  /// Call this in your app's main() function before runApp()
  static Future<void> initialize() async {
    if (_initialized) return;
    
    print('Initializing phone_form_field package - preloading country flags...');
    await _preloadCountryFlags();
    _initialized = true;
    print('phone_form_field package initialization complete');
  }
  
  static Future<void> _preloadCountryFlags() async {
    try {
      CircleFlag.preload(['fr', 'us']);
      CircleFlag.cache;
      print('CircleFlag preloaded France and US flags');
    } catch (e) {
      print('Failed to preload CircleFlag flags: $e');
    }
  }
}

Strangely, this has yielded no results either, as clicking the dialog when offline doesn't render any of the SVGs I need. I would appreciate any help that can be provided to help me accomplish this task based on point 3.

The example code I am using as given below:

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

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize the package to preload SVGs
  await PhoneNumberInputPackage.initialize();
  
  runApp(const MyApp());
}

// this example makes uses of lots of properties that would not be there
// in a real scenario for the sake of showing the features.
// For a simpler example see the README

class PhoneFieldView extends StatelessWidget {
  static const supportedLocales = [
    Locale('ar'),
    // not supported by material yet
    // Locale('ckb'),
    Locale('de'),
    Locale('el'),
    Locale('en'),
    Locale('es'),
    Locale('fa'),
    Locale('fr'),
    Locale('hi'),
    Locale('hu'),
    Locale('it'),
    // not supported by material yet
    // Locale('ku'),
    Locale('nb'),
    Locale('nl'),
    Locale('pt'),
    Locale('ru'),
    Locale('sv'),
    Locale('tr'),
    Locale('uz'),
    Locale('zh'),
    // ...
  ];

  final PhoneController controller;
  final FocusNode focusNode;
  final CountrySelectorNavigator selectorNavigator;
  final bool withLabel;
  final bool outlineBorder;
  final bool isCountryButtonPersistant;
  final bool mobileOnly;
  final Locale locale;

  const PhoneFieldView({
    Key? key,
    required this.controller,
    required this.focusNode,
    required this.selectorNavigator,
    required this.withLabel,
    required this.outlineBorder,
    required this.isCountryButtonPersistant,
    required this.mobileOnly,
    required this.locale,
  }) : super(key: key);

  PhoneNumberInputValidator? _getValidator(BuildContext context) {
    List<PhoneNumberInputValidator> validators = [];
    if (mobileOnly) {
      validators.add(PhoneValidator.validMobile(context));
    } else {
      validators.add(PhoneValidator.valid(context));
    }
    return validators.isNotEmpty ? PhoneValidator.compose(validators) : null;
  }

  @override
  Widget build(BuildContext context) {
    return AutofillGroup(
      child: Localizations.override(
        context: context,
        locale: locale,
        child: Builder(
          builder: (context) {
            final label = PhoneFieldLocalization.of(context).phoneNumber;
            return PhoneFormField(
              focusNode: focusNode,
              controller: controller,
              isCountryButtonPersistent: isCountryButtonPersistant,
              autofocus: false,
              autofillHints: const [AutofillHints.telephoneNumber],
              countrySelectorNavigator: selectorNavigator,
              decoration: InputDecoration(
                  label: withLabel ? Text(label) : null,
                  border: outlineBorder
                      ? OutlineInputBorder(
                          borderRadius: BorderRadius.circular(50))
                      : const UnderlineInputBorder(),
                  hintText: withLabel ? '' : label,
                  contentPadding: const EdgeInsets.all(0)),
              enabled: true,
              countryButtonStyle: CountryButtonStyle(
                  showFlag: true,
                  showIsoCode: false,
                  showDialCode: true,
                  showDropdownIcon: true,
                  borderRadius: BorderRadius.circular(50)),

              validator: _getValidator(context),
              autovalidateMode: AutovalidateMode.onUserInteraction,
              cursorColor: Theme.of(context).colorScheme.primary,
              // ignore: avoid_print
              onSaved: (p) => print('saved $p'),
              // ignore: avoid_print
              onChanged: (p) => print('changed $p'),
            );
          },
        ),
      ),
    );
  }
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: PhoneFieldLocalization.delegates,
      supportedLocales: PhoneFieldView.supportedLocales,
      locale: const Locale('en'),
      title: 'Phone field demo',
      theme: ThemeData(
        brightness: Brightness.dark,
        primarySwatch: Colors.blue,
      ),
      home: const PhoneFormFieldScreen(),
    );
  }
}

class PhoneFormFieldScreen extends StatefulWidget {
  const PhoneFormFieldScreen({Key? key}) : super(key: key);

  @override
  PhoneFormFieldScreenState createState() => PhoneFormFieldScreenState();
}

class PhoneFormFieldScreenState extends State<PhoneFormFieldScreen> {
  late PhoneController controller;
  final FocusNode focusNode = FocusNode();

  bool outlineBorder = true;
  bool mobileOnly = true;
  bool isCountryButtonPersistent = true;
  bool withLabel = true;
  CountrySelectorNavigator selectorNavigator =
      const CountrySelectorNavigator.page();
  Locale locale = const Locale('en');
  final formKey = GlobalKey<FormState>();

  @override
  initState() {
    super.initState();
    controller = PhoneController();
    controller.addListener(() => setState(() {}));
  }

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

  void _showPhoneDialog() {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext dialogContext) {
        return StatefulBuilder(
          builder: (context, setDialogState) {
            return Dialog(
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(16),
              ),
              child: Container(
                width: MediaQuery.of(context).size.width * 0.9,
                constraints: const BoxConstraints(maxWidth: 500, maxHeight: 600),
                padding: const EdgeInsets.all(24.0),
                child: SingleChildScrollView(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      // Dialog Header
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          const Text(
                            'Enter Phone Number',
                            style: TextStyle(
                              fontSize: 20,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                          IconButton(
                            onPressed: () => Navigator.of(dialogContext).pop(),
                            icon: const Icon(Icons.close),
                          ),
                        ],
                      ),
                      const SizedBox(height: 20),
                      
                      // Phone Field
                      Form(
                        key: formKey,
                        child: PhoneFieldView(
                          controller: controller,
                          focusNode: focusNode,
                          selectorNavigator: selectorNavigator,
                          withLabel: withLabel,
                          outlineBorder: outlineBorder,
                          isCountryButtonPersistant: isCountryButtonPersistent,
                          mobileOnly: mobileOnly,
                          locale: locale,
                        ),
                      ),
                      
                      const SizedBox(height: 16),
                      
                      // Phone value display
                      Container(
                        width: double.infinity,
                        padding: const EdgeInsets.all(12),
                        decoration: BoxDecoration(
                          color: Theme.of(context).colorScheme.surface,
                          borderRadius: BorderRadius.circular(8),
                          border: Border.all(
                            color: Theme.of(context).colorScheme.outline,
                          ),
                        ),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              'Phone Number Details:',
                              style: TextStyle(
                                fontWeight: FontWeight.bold,
                                color: Theme.of(context).colorScheme.primary,
                              ),
                            ),
                            const SizedBox(height: 8),
                            Text('Value: ${controller.value.toString()}'),
                            Text('Valid mobile: ${controller.value.isValid(type: PhoneNumberType.mobile)}'),
                            Text('Valid fixed line: ${controller.value.isValid(type: PhoneNumberType.fixedLine)}'),
                          ],
                        ),
                      ),
                      
                      const SizedBox(height: 20),
                      
                      // Action buttons
                      Wrap(
                        spacing: 8,
                        runSpacing: 8,
                        children: [
                          ElevatedButton(
                            onPressed: () {
                              formKey.currentState?.reset();
                              setDialogState(() {}); // Update dialog state
                            },
                            child: const Text('Reset'),
                          ),
                          ElevatedButton(
                            onPressed: () {
                              controller.selectNationalNumber();
                              focusNode.requestFocus();
                            },
                            child: const Text('Select Number'),
                          ),
                          ElevatedButton(
                            onPressed: () {
                              controller.value = PhoneNumber.parse('+33 699 999 999');
                              setDialogState(() {}); // Update dialog state
                            },
                            child: const Text('Set Example'),
                          ),
                        ],
                      ),
                      
                      const SizedBox(height: 20),
                      
                      // Save/Cancel buttons
                      Row(
                        mainAxisAlignment: MainAxisAlignment.end,
                        children: [
                          TextButton(
                            onPressed: () => Navigator.of(dialogContext).pop(),
                            child: const Text('Cancel'),
                          ),
                          const SizedBox(width: 8),
                          ElevatedButton(
                            onPressed: () {
                              // Handle save/submit action
                              if (controller.value.isValid()) {
                                print('Phone number saved: ${controller.value}');
                                Navigator.of(dialogContext).pop();
                                ScaffoldMessenger.of(context).showSnackBar(
                                  SnackBar(
                                    content: Text('Phone number saved: ${controller.value}'),
                                    backgroundColor: Colors.green,
                                  ),
                                );
                              } else {
                                ScaffoldMessenger.of(context).showSnackBar(
                                  const SnackBar(
                                    content: Text('Please enter a valid phone number'),
                                    backgroundColor: Colors.red,
                                  ),
                                );
                              }
                            },
                            child: const Text('Save'),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ),
            );
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Phone_form_field'),
      ),
      body: SingleChildScrollView(
        child: Center(
          child: Container(
            constraints: const BoxConstraints(maxWidth: 600),
            child: Card(
              child: Padding(
                padding: const EdgeInsets.all(20.0),
                child: Column(
                  children: [
                    // Title
                    const Text(
                      'Phone Form Field Configuration',
                      style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 20),
                    
                    // Configuration switches
                    SwitchListTile(
                      value: outlineBorder,
                      onChanged: (v) => setState(() => outlineBorder = v),
                      title: const Text('Outlined border'),
                    ),
                    SwitchListTile(
                      value: withLabel,
                      onChanged: (v) => setState(() => withLabel = v),
                      title: const Text('Label'),
                    ),
                    SwitchListTile(
                      value: isCountryButtonPersistent,
                      onChanged: (v) =>
                          setState(() => isCountryButtonPersistent = v),
                      title: const Text('Persistent country chip'),
                    ),
                    SwitchListTile(
                      value: mobileOnly,
                      onChanged: (v) => setState(() => mobileOnly = v),
                      title: const Text('Mobile phone number only'),
                    ),
                    
                    const Divider(),
                    
                    // Language selector
                    ListTile(
                      title: const Text('Language'),
                      trailing: DropdownButton<Locale>(
                        value: locale,
                        onChanged: (Locale? value) {
                          if (value != null) {
                            setState(() => locale = value);
                          }
                        },
                        items: [
                          for (final locale in PhoneFieldView.supportedLocales)
                            DropdownMenuItem(
                              value: locale,
                              child: Text(locale.toLanguageTag()),
                            ),
                        ],
                      ),
                    ),
                    
                    // Country selector navigator
                    ListTile(
                      title: const Text('Country selector'),
                      trailing: DropdownButton<CountrySelectorNavigator>(
                        value: selectorNavigator,
                        onChanged: (CountrySelectorNavigator? value) {
                          if (value != null) {
                            setState(() => selectorNavigator = value);
                          }
                        },
                        items: const [
                          DropdownMenuItem(
                            value: CountrySelectorNavigator.bottomSheet(
                                favorites: [IsoCode.GU, IsoCode.GY]),
                            child: Text('Bottom sheet'),
                          ),
                          DropdownMenuItem(
                            value: CountrySelectorNavigator.draggableBottomSheet(),
                            child: Text('Draggable modal sheet'),
                          ),
                          DropdownMenuItem(
                            value: CountrySelectorNavigator.modalBottomSheet(),
                            child: Text('Modal sheet'),
                          ),
                          DropdownMenuItem(
                            value: CountrySelectorNavigator.dialog(width: 720),
                            child: Text('Dialog'),
                          ),
                          DropdownMenuItem(
                            value: CountrySelectorNavigator.page(),
                            child: Text('Page'),
                          ),
                        ],
                      ),
                    ),
                    
                    const SizedBox(height: 30),
                    
                    // Button to open dialog
                    Container(
                      width: double.infinity,
                      child: ElevatedButton.icon(
                        onPressed: _showPhoneDialog,
                        icon: const Icon(Icons.phone, size: 24),
                        label: const Text(
                          'Open Phone Number Dialog',
                          style: TextStyle(fontSize: 16),
                        ),
                        style: ElevatedButton.styleFrom(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 24,
                            vertical: 16,
                          ),
                          shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(12),
                          ),
                        ),
                      ),
                    ),
                    
                    const SizedBox(height: 20),
                    
                    // Current phone number display
                    Container(
                      width: double.infinity,
                      padding: const EdgeInsets.all(16),
                      decoration: BoxDecoration(
                        color: Theme.of(context).colorScheme.surface,
                        borderRadius: BorderRadius.circular(12),
                        border: Border.all(
                          color: Theme.of(context).colorScheme.outline,
                        ),
                      ),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            'Current Phone Number:',
                            style: TextStyle(
                              fontWeight: FontWeight.bold,
                              fontSize: 16,
                              color: Theme.of(context).colorScheme.primary,
                            ),
                          ),
                          const SizedBox(height: 12),
                          Container(
                            padding: const EdgeInsets.all(12),
                            decoration: BoxDecoration(
                              color: Theme.of(context).colorScheme.background,
                              borderRadius: BorderRadius.circular(8),
                            ),
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(
                                  'Value: ${controller.value.toString()}',
                                  style: const TextStyle(fontFamily: 'monospace'),
                                ),
                                const SizedBox(height: 4),
                                Row(
                                  children: [
                                    Icon(
                                      controller.value.isValid(type: PhoneNumberType.mobile) 
                                          ? Icons.check_circle 
                                          : Icons.cancel,
                                      size: 16,
                                      color: controller.value.isValid(type: PhoneNumberType.mobile) 
                                          ? Colors.green 
                                          : Colors.red,
                                    ),
                                    const SizedBox(width: 4),
                                    Text('Valid mobile: ${controller.value.isValid(type: PhoneNumberType.mobile)}'),
                                  ],
                                ),
                                const SizedBox(height: 4),
                                Row(
                                  children: [
                                    Icon(
                                      controller.value.isValid(type: PhoneNumberType.fixedLine) 
                                          ? Icons.check_circle 
                                          : Icons.cancel,
                                      size: 16,
                                      color: controller.value.isValid(type: PhoneNumberType.fixedLine) 
                                          ? Colors.green 
                                          : Colors.red,
                                    ),
                                    const SizedBox(width: 4),
                                    Text('Valid fixed line: ${controller.value.isValid(type: PhoneNumberType.fixedLine)}'),
                                  ],
                                ),
                              ],
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions