flutterradio-buttonsharedpreferencespreferences

shared preferences does not save radio button checkmark in Flutter


I implemented the shared preferences package in my Flutter app, with a list widget as radio button, that only save the language preference and not the checkmark. So when i close the Language screen and come back, the language checkmark goes the the default one even if the language, saved in shared preferences is French or Italian.

This is my Language screen:

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

@override
State<LanguagesScreen> createState() => _LanguagesScreenState();
}

class Item {
 final String prefix;
 final String? helper;
 const Item({required this.prefix, this.helper});
}

var items = [
   Item(prefix: 'English', helper: 'English',), //value: 'English'
   Item(prefix: 'Français', helper: 'French'),
   Item(prefix: 'Italiano', helper: 'Italian'),
  ];

  class _LanguagesScreenState extends State<LanguagesScreen> {

  var _selectedIndex = 0;
  final _userPref = UserPreferences();
  var _selecLangIndex;

  int index = 0;
  final List<String> entries = <String>['English', 'French', 'Italian'];*/

  //init shared preferences
  @override
  void initState() {
   super .initState();
   _populateField();
  }

 void _populateField() async {
  var prefSettings = await _userPref.getPrefSettings();
  setState((){
    _selecLangIndex = prefSettings.language;
  });
 }

@override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: AppBar(...
      ),
      body: CupertinoPageScaffold(
        child: Container(
          child: SingleChildScrollView(
                child: CupertinoFormSection.insetGrouped(
                    children: [
                      ...List.generate(items.length, (index) => GestureDetector(
                          onTap: () async {
                            setState(() => _selectedIndex = index);
                            if (index == 0){
                              await context.setLocale(Locale('en','US'));
                              _selecIndex = Language.English;
                            }
                            else if (index == 1){
                              await context.setLocale(Locale('fr','FR'));
                              _selecIndex = Language.French;

                            }
                          child: buildCupertinoFormRow(
                            items[index].prefix,
                            items[index].helper,
                            selected: _selectedIndex == index,
                          )
                      )),
                      TextButton(onPressed:
                        _saveSettings,
                        child: Text('save', 
                      )

  buildCupertinoFormRow(String prefix, String? helper, {bool selected = false,}) {
    return CupertinoFormRow(
        prefix: Text(prefix),
        helper: helper != null
        ? Text(helper, style: Theme.of(context).textTheme.bodySmall,)
            :null, child: selected ? const Icon(CupertinoIcons.check_mark,
      color: Colors.blue, size: 20,) :Container(),
    );
  }

  void _saveSettings() {
    final newSettings = PrefSettings(language:_selecIndex);
    _userPref.saveSettings(newSettings);
    Navigator.pop(context);
  }
}

this is the UserPreference:

   class UserPreferences {
   Future saveSettings(PrefSettings prefSettings) async {
    final preferences = await SharedPreferences.getInstance();

    await preferences.setInt('language' , prefSettings.language.index );
  }

  Future<PrefSettings> getPrefSettings() async {
    final preferences = await SharedPreferences.getInstance();

    final language = Language.values[preferences.getInt('language') ?? 0 ];

    return PrefSettings(language: language);
  }
}

enum Language { English, French, Italian}

class PrefSettings{
  final Language language;

  PrefSettings (
      {required this.language});
}

Solution

  • I'm betting that the issue is in initState. You are calling _populateField, but it doesn't complete before building because it's an async method, and you can't await for it: so the widget gets build, loading the default position for the checkmark, and only after that _populateField completes...but then it's too late to show the saved data correctly.

    In my experience, if I have not already instantiated a SharedPreferences object somewhere else in the code, I use this to load it:

    class _LanguagesScreenState extends State<LanguagesScreen> {
    
    [...]
    
    @override
      Widget build(BuildContext context) {
        return FutureBuilder(
            //you can put any async method here, just be 
            //sure that you use the type it returns later when using 'snapshot.data as T'
            future: await SharedPreferences.getInstance(), 
            builder: (context, snapshot) {
              //error handling
              if (!snapshot.hasData || snapshot.connectionState != ConnectionState.done) {
                return const Center(child: CircularProgressIndicator());
              } else if (snapshot.hasError) {
                return Center(child: Text(snapshot.error.toString()));
              }
              var prefs= snapshot.data as SharedPreferences;
              //now you have all the preferences available without need to await for them
              return Scaffold((
                 [...]
              );
    

    EDIT

    I started writing another comment, but there are so many options here that there wasn't enough space.

    First, the code I posted should go in your _LanguagesScreenState build method. The FutureBuilder I suggested should wrap anything that depends on the Future you must wait for to complete. I put it up at the root, above Scaffold, but you can move it down the widgets' tree as you need, just remember that everything that needs to read the preferences has to be inside the FutureBuilder.

    Second, regarding SharedPreferences.getInstance(), there are two ways: the first is declaring it as a global variable, and loading it even in the main method where everything starts. By doing this you'll be able to reference it from anywhere in your code, just be careful to save the changes everytime is needed. The second is to load it everytime you need, but you'll end up using a FutureBuilder a lot. I don't know if any of these two options is better than the other: the first might have problems if somehow the SharedPreferences object gets lost, while the second requires quite more code to work.