0

I have a Flutter application where users can select multiple options from a dropdown using the dropdown_search package. I would like to add a feature where the selected items are cached locally, and then pre-populated when the user comes back to the application.

I tried using the shared_preferences package to store the selected items in the local storage of the device. However, I encountered an error when attempting to save the selected choices asynchronously within the onItemAdded and onItemRemoved callbacks of the DropdownSearch widget.

Here's the code:

import 'package:flutter/material.dart';
import 'package:dropdown_search/dropdown_search.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';


void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final preferences = await SharedPreferences.getInstance();
  runApp(MyApp(preferences: preferences));
}

class MyApp extends StatelessWidget {
  final SharedPreferences preferences;
  const MyApp({required this.preferences, Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'MultiSelectDropDown',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'MultiSelectDropDown'),
      initialBinding: QuestionnaireBinding(preferences: preferences),
    );
  }
}

class QuestionnaireBinding extends Bindings {
  final SharedPreferences preferences;

  QuestionnaireBinding({required this.preferences});
  @override
  void dependencies() {
    Get.lazyPut<QuestionnaireController>(() => QuestionnaireController(preferences: preferences));
  }
}

class QuestionnaireController extends GetxController
    with GetSingleTickerProviderStateMixin {
  RxList<String> choices = <String>[].obs;
  Rx<bool> showBackButton = true.obs;
  final SharedPreferences preferences;

  QuestionnaireController({required this.preferences});

  @override
  void onInit() {
    super.onInit();
    loadSelectedChoices();
  }

  Future<void> saveSelectedChoices() async {
    print("saving");
    preferences.setStringList('selected_choices', choices.toList());
    print(choices.toList());
  }

  Future<void> loadSelectedChoices() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    List<String>? loadedChoices = prefs.getStringList('selected_choices');
    if (loadedChoices != null) {
      print("loading choices");
      print(choices.value);
      choices.value = loadedChoices.obs;
    }
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final dropdownSearchTheme = ThemeData(
      colorScheme: const ColorScheme(
    primary: Color(0xFF485946),
    onPrimary: Colors.white,
    secondary: Colors.black,
    onSecondary: Colors.deepOrange,
    error: Colors.transparent,
    onError: Colors.transparent,
    background: Colors.transparent,
    onBackground: Colors.black,
    brightness: Brightness.light,
    onSurface: Colors.black,
    surface: Colors.white,
  ));

  @override
  Widget build(BuildContext context) {
    return GetBuilder<QuestionnaireController>(builder: (controller) {
      return Scaffold(
        appBar: AppBar(
          leading: Obx(() => controller.showBackButton.value ? BackButton() : SizedBox.shrink()),
          // Use the new variable to control the visibility of the back button
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: Text(widget.title),
        ),
        body: SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                const Text(
                  'Let\'s start choosing',
                  textAlign: TextAlign.center,
                ),
                const SizedBox(
                  height: 30,
                ),
                Theme(
                  data: dropdownSearchTheme,
                  child: _buildDropdownSearch(
                    context,
                    Get.find<QuestionnaireController>().choices,
                    dropdownCustomBuilder: _customChoice,
                  ),
                ),
              ],
            ),
          ),
        ),
      );
    });
  }




  Widget _buildDropdownSearch(
      BuildContext context, RxList<String> selectedChoice,
      {Widget Function(BuildContext, List<String>)? dropdownCustomBuilder}) {
    final controller = Get.find<QuestionnaireController>();

    // Initialize FocusNode and add a listener
    FocusNode _dropdownFocusNode = FocusNode();
    _dropdownFocusNode.addListener(() {
      if (_dropdownFocusNode.hasFocus) {
        // Dropdown is opened
        controller.showBackButton.value = false;
      } else {
          controller.showBackButton.value = true;
      }
    });

    return DropdownSearch<String>.multiSelection(
      selectedItems: selectedChoice,
      items: [
        "choice1",
        "choice2",
        "choice3",
        "choice4",
        "choice5",
        "choice6",
        "choice7",
        "choice8",
        "choice9",
        "choice10",
        "choice11"
      ],
      dropdownDecoratorProps: DropDownDecoratorProps(
          dropdownSearchDecoration: InputDecoration(
        hintText: 'pickChoice',
        hintStyle: TextStyle(color: Colors.black),
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(8.0),
          borderSide: BorderSide(
            color: Colors.grey,
          ),
        ),
      )),
      dropdownBuilder: dropdownCustomBuilder,
      popupProps: PopupPropsMultiSelection.bottomSheet(
        searchFieldProps: TextFieldProps(
          padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
          decoration: InputDecoration(
            hintText: 'Search for choice',
            hintStyle: TextStyle(color: Colors.black),
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(8.0),
              borderSide: BorderSide(
                color: Colors.grey,
              ),
            ),
            suffixIcon: IconButton(
              icon: Icon(Icons.clear),
              onPressed: () {
                controller.choices.clear();
              },
            ),
          ),
          style: TextStyle(color: Colors.black),
          autofocus: true,
          focusNode: _dropdownFocusNode,
        ),
        showSearchBox: true,
        showSelectedItems: true,
        isFilterOnline: false,
        onItemAdded: (selectedItems, addedItem) {
          controller.choices.add(addedItem); // Change selectedChoice to controller.choices
          controller.saveSelectedChoices(); // Save choices to preferences
        },
        onItemRemoved: (selectedItems, removedItem) {
          controller.choices.remove(removedItem); // Change selectedChoice to controller.choices
          controller.saveSelectedChoices(); // Save choices to preferences
        },
      ),
    );
  }

  Widget _customChoice(BuildContext context, List<String> selectedItems) {
    final controller = Get.find<QuestionnaireController>();
    return GetBuilder<QuestionnaireController>(
      initState: (_) {},
      builder: (_) {
        if (selectedItems.isEmpty) {
          return Text("Pick a choice");
        }
        return Wrap(
          spacing: 2,
          runSpacing: -10,
          children: selectedItems.map((e) {
            return Chip(
              backgroundColor: Colors.lightGreen,
              label: Text(
                e.toString().trim(),
              ),
              onDeleted: () {
                selectedItems.remove(e.toString().trim());
                controller.choices.remove(e.toString().trim());
              },
            );
          }).toList(),
        );
      },
    );
  }
}

The error I receive is:

Lookup failed: SharedPreferences in package:shared_preferences/shared_preferences.dart

I understand that the callbacks are not designed to handle asynchronous operations, so I'm looking for guidance on the best way to achieve this feature.

How can I properly cache the selected items from the multi-select dropdown in Flutter and load them when the user returns to the application?

Gladiator
  • 354
  • 3
  • 19

1 Answers1

0

Load your data before the application launches:

void main() async { // <= async main
  final preferences = await SharedPreferences.getInstance();
  final data = ... // get your saved data
  runApp(MyApp(data: data)); // pass data from preferences to your app
}

The callbacks don't have to await saving to preferences: fire and forget your save operations.

offworldwelcome
  • 1,314
  • 5
  • 11
  • Thank you for the response. I tried the code. Though the data is being saved in sharedPrefs, it does not reflect in the controller. Every time the controller beings with 'Pick a choice'. I have now updated my above code according to your suggestion. Can you please let me know where I am going wrong? – Gladiator Aug 10 '23 at 17:56
  • Did the controller's `loadSelectedChoices` method finish before `_MyHomePageState` accesses it? Use some `log` or `print` to verify. – offworldwelcome Aug 10 '23 at 18:21