AhmedLSayed9 / dropdown_button2

Flutter's core Dropdown Button widget with steady dropdown menu and many other features.
https://pub.dev/packages/dropdown_button2
MIT License
264 stars 122 forks source link

DropdownButtonFormField2 initial value #281

Closed MuKhAlm closed 1 month ago

MuKhAlm commented 3 months ago

I am having trouble setting a default/initial value for [DropdownButtonFormField2].

I somehow got the [DropdownMenuItem]s to have initial values. However, I could not get the field that shows the items selected to have an initial value.

Is there a way for me to assign initial values for the Widget or is there no such feature?

Here is my code if that helps:

import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:edusafe_management/src/features/subjects/domain/subject.dart';
import 'package:edusafe_management/src/features/subjects/presentation/subjects_controller.dart';
import 'package:edusafe_management/src/localisation/string_hardcoded.dart';
import 'package:edusafe_management/src/utils/show_error_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

List<Subject> _selectedItems = [];

class SubjectsField extends ConsumerStatefulWidget {
  const SubjectsField({
    super.key,
    required this.onChanged,
    this.initialValue,
  });

  final void Function(List<Subject>) onChanged;
  final List<Subject>? initialValue;

  @override
  ConsumerState<SubjectsField> createState() => _SubjectsFieldState();
}

class _SubjectsFieldState extends ConsumerState<SubjectsField> {
  @override
  Widget build(BuildContext context) {
    ref.listen(subjectsControllerProvider, (_, state) {
      if (state.hasError && !state.isLoading) {
        showErrorDialog(context, state);
      }
    });

    final subjects = ref.watch(subjectsControllerProvider);

    final List<Subject> items = !subjects.hasValue ? [] : subjects.value!;

    return DropdownButtonFormField2<Subject>(
      isExpanded: true,
      hint: Text('Subjects'.hardcoded),
      items: items.map((subject) {
        return DropdownMenuItem(
          value: subject,
          //disable default onTap to avoid closing menu when selecting an item
          enabled: false,
          child: StatefulBuilder(
            builder: (context, menuSetState) {
              // Initialising _selectedItems
              for (var s in widget.initialValue!) {
                if (subject.id == s.id) {
                  if (!_selectedItems.contains(subject)) {
                    _selectedItems.add(subject);
                  }
                }
              }
              final isSelected = _selectedItems.contains(subject);
              return InkWell(
                onTap: () {
                  isSelected
                      ? _selectedItems.remove(subject)
                      : _selectedItems.add(subject);
                  //This rebuilds the StatefulWidget to update the button's text
                  setState(() {});
                  widget.onChanged(_selectedItems);
                  //This rebuilds the dropdownMenu Widget to update the check mark
                  menuSetState(() {});
                },
                child: Container(
                  height: double.infinity,
                  padding: const EdgeInsets.symmetric(horizontal: 16.0),
                  child: Row(
                    children: [
                      if (isSelected)
                        const Icon(Icons.check_box_outlined)
                      else
                        const Icon(Icons.check_box_outline_blank),
                      const SizedBox(width: 16),
                      Expanded(
                        child: Text(subject.name),
                      ),
                    ],
                  ),
                ),
              );
            },
          ),
        );
      }).toList(),
      //Use last selected item as the current value so if we've limited menu height, it scroll to last item.
      value: _selectedItems.isEmpty ? null : _selectedItems.last,
      onChanged: (value) {},
      selectedItemBuilder: (context) {
        return items.map(
          (item) {
            return Container(
              alignment: AlignmentDirectional.centerStart,
              child: Text(
                _selectedItems.join(' - '.hardcoded),
                style: const TextStyle(
                  overflow: TextOverflow.ellipsis,
                ),
                maxLines: 1,
              ),
            );
          },
        ).toList();
      },
      validator: (value) {
        if (_selectedItems.isEmpty) {
          return 'Subjects can\'t be empty'.hardcoded;
        }
        return null;
      },
      menuItemStyleData: const MenuItemStyleData(
        height: 40,
        padding: EdgeInsets.zero,
      ),
    );
  }
}
Shashwat-111 commented 1 month ago

I am having the similar problem. Were you able to find a solution ?

MuKhAlm commented 1 month ago

I found a workaround, here it goes:

Assuming you use the same template shown above (and in their examples), you will have two lists. items for all items to display and select and unselect from. selectedItems, for well, selected items.

At the start of the Widget build method (SubjectField build method in my example) you have to initialize isSelected with the items from the items list which you want to initialize with.

In my case it became the following:

for (final subject in items) {
        for (final s in widget.initialValue!) {
          if (subject.id == s.id) {
            if (!selectedItems.contains(subject)) {
              selectedItems.add(subject);
            }
          }
        }
      }

The solution might not be the best. But it works. I'd advice you to give FormBuilder package a try. They also have their own version of a multi-value drop-down field.

If you are still have trouble, please do tell. It'd be my pleasure to help ^_^