How do I make a single DropDownButton widget be used multiple times to save space?

1

I have a registration form with many DropDownButton fields that take enums in the menu. Currently, I'm making a separate widget for every single button. Is there a way to just make a single widget and then using its parameters to change it?

Is there a better way to do this? Currently, I'm just copy-pasting and renaming it for every DropDownButton I want to create.

class _BloodTypeDropDownFieldState extends State<BloodTypeDropDownField> {
  BloodType _currentSelectedValue;
  @override
  Widget build(BuildContext context) {
    return DropdownButtonHideUnderline(
      child: DropdownButton<BloodType>(
        value: _currentSelectedValue,
        hint: Text(
          "Blood Group",
          style: GoogleFonts.roboto(
            textStyle: Theme.of(context).textTheme.headline4,
            fontSize: 13,
            fontWeight: FontWeight.w700,
            color: Color(0xffffffff),
          ),
        ),
        isDense: true,
        onChanged: (BloodType newValue) {
          setState(() {
            _currentSelectedValue = newValue;
          });
        },
        selectedItemBuilder: (BuildContext context) {
          return BloodType.getValues().map((BloodType bloodType) {
            return Text(
              bloodType.toString(),
              style: GoogleFonts.roboto(
                textStyle: Theme.of(context).textTheme.headline4,
                fontSize: 13,
                fontWeight: FontWeight.w700,
                color: Color(0xffffffff),
              ),
            );
          }).toList();
        },
        items: BloodType.getValues().map((BloodType bloodType) {
          return DropdownMenuItem<BloodType>(
            value: bloodType,
            child: Text(
              bloodType.toString(),
              style: GoogleFonts.roboto(
                textStyle: Theme.of(context).textTheme.headline4,
                fontSize: 15,
                fontWeight: FontWeight.w500,
                color: Colors.black,
              ),
            ),
          );
        }).toList()
      ),
    );
  }
}

class BloodType {
  final value;

  const BloodType._internal(this.value);

  toString() => '$value';

  static const A_PLUS = const BloodType._internal('A+');
  static const A_MINUS = const BloodType._internal('A-');
  static const B_PLUS = const BloodType._internal('B+');
  static const B_MINUS = const BloodType._internal('B-');
  static const AB_PLUS = const BloodType._internal('AB+');
  static const AB_MINUS = const BloodType._internal('AB-');
  static const O_PLUS = const BloodType._internal('O+');
  static const O_MINUS = const BloodType._internal('O-');

  static List<BloodType> list = [ 
    A_PLUS,
    A_MINUS,
    B_PLUS,
    B_MINUS,
    AB_PLUS,
    AB_MINUS,
    O_PLUS,
    O_MINUS
  ];
  static List<BloodType> getValues() => list;
}
flutter
asked on Stack Overflow Feb 6, 2021 by SuperZecton • edited Feb 7, 2021 by Thierry

2 Answers

0

You could define a generic class for your custom DropDown:

class MyDropDown<T> extends StatelessWidget {
  final String hint;
  final T value;
  final List<T> values;
  final ValueChanged<T> onChanged;

  MyDropDown({
    Key key,
    this.hint,
    this.value,
    @required this.values,
    this.onChanged,
  })  : assert(values != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return DropdownButtonHideUnderline(
      child: DropdownButton<T>(
        value: value,
        hint: Text(hint ?? 'Pick one'),
        isDense: true,
        onChanged: onChanged,
        items: values.map((value) => DropdownMenuItem<T>(
              value: value,
              child: Text(value.toString()),
            ))
        .toList(),
      ),
    );
  }

}

That you can easily use wherever you want:

class HomePage extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final _current = useState<BloodType>();
    return Scaffold(
      body: Center(
        child: MyDropDown<BloodType>(
          hint: 'Blood Type',
          value: _current.value,
          values: bloodTypes,
          onChanged: (value) => _current.value = value,
        ),
      ),
    );
  }
}

For whatever type T. Here is an example for your BloodTypes:

enum BloodGroupEnum {
  a,
  b,
  ab,
  o,
}

extension BloodGroupEnumX on BloodGroupEnum {
  String get label => describeEnum(this).toUpperCase();
}

enum RhdEnum {
  positive,
  negative,
}

extension RhdEnumX on RhdEnum {
  String get label {
    switch (this) {
      case RhdEnum.positive:
        return '+';
      case RhdEnum.negative:
        return '-';
      default:
        return '';
    }
  }
}

class BloodType {
  final BloodGroupEnum group;
  final RhdEnum rhd;

  String toString() => '${group.label}${rhd.label}';

  BloodType({
    this.group,
    this.rhd,
  });
}

final List<BloodType> bloodTypes = BloodGroupEnum.values
    .map((group) => RhdEnum.values.map((rhd) {
          print('NEW');
          print(BloodType(group: group, rhd: rhd).toString());
          return BloodType(group: group, rhd: rhd);
        }).toList())
    .expand((i) => i)
    .toList();

The only requirement for the class you want to use within MyDropDown is to properly implement toString() that is used as a label for the DropdownMenuItem.


Full source code for easy copy-paste:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:google_fonts/google_fonts.dart';

void main() {
  runApp(AppWidget());
}

class AppWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(
        textTheme: GoogleFonts.robotoTextTheme(
          Theme.of(context).textTheme,
        ),
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final _current = useState<BloodType>();
    return Scaffold(
      body: Center(
        child: MyDropDown<BloodType>(
          hint: 'Blood Type',
          value: _current.value,
          values: bloodTypes,
          onChanged: (value) => _current.value = value,
        ),
      ),
    );
  }
}

class MyDropDown<T> extends StatelessWidget {
  final String hint;
  final T value;
  final List<T> values;
  final ValueChanged<T> onChanged;

  MyDropDown({
    Key key,
    this.hint,
    this.value,
    @required this.values,
    this.onChanged,
  })  : assert(values != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return DropdownButtonHideUnderline(
      child: DropdownButton<T>(
        value: value,
        hint: Text(hint ?? 'Pick one'),
        isDense: true,
        onChanged: onChanged,
        items: values
            .map((value) => DropdownMenuItem<T>(
                  value: value,
                  child: Text(value.toString()),
                ))
            .toList(),
      ),
    );
  }
}

enum BloodGroupEnum {
  a,
  b,
  ab,
  o,
}

extension BloodGroupEnumX on BloodGroupEnum {
  String get label => describeEnum(this).toUpperCase();
}

enum RhdEnum {
  positive,
  negative,
}

extension RhdEnumX on RhdEnum {
  String get label {
    switch (this) {
      case RhdEnum.positive:
        return '+';
      case RhdEnum.negative:
        return '-';
      default:
        return '';
    }
  }
}

class BloodType {
  final BloodGroupEnum group;
  final RhdEnum rhd;

  String toString() => '${group.label}${rhd.label}';

  BloodType({
    this.group,
    this.rhd,
  });
}

final List<BloodType> bloodTypes = BloodGroupEnum.values
    .map((group) =>
        RhdEnum.values.map((rhd) => BloodType(group: group, rhd: rhd)).toList())
    .expand((i) => i)
    .toList();
answered on Stack Overflow Feb 6, 2021 by Thierry
-1

So I think what you are looking for is to reuse the dropdownbutton I would recommend to use a callback that will give back the type that is selected in the Widget to it's parent. We can use the ValueChanged.

class CustomDropDown extends StatelessWidget {

  final ValueChanged<BloodType> onChanged;
  final BloodType currentSelectedValue;

  const CustomDropDown({Key key, @required this.onChanged, @required this.currentSelectedValue}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return DropdownButtonHideUnderline(
          child: DropdownButton<BloodType>(
            value: currentSelectedValue,
            hint: Text(
              "Blood Group",
              style: GoogleFonts.roboto(
                textStyle: Theme.of(context).textTheme.headline4,
                fontSize: 13,
                fontWeight: FontWeight.w700,
                color: Color(0xffffffff),
              ),
            ),
            isDense: true,
            onChanged: this.onChanged,
            selectedItemBuilder: (BuildContext context) {
              return BloodType.getValues().map((BloodType bloodType) {
                return Text(
                  bloodType.toString(),
                  style: GoogleFonts.roboto(
                    textStyle: Theme.of(context).textTheme.headline4,
                    fontSize: 13,
                    fontWeight: FontWeight.w700,
                    color: Color(0xffffffff),
                  ),
                );
              }).toList();
            },
            items: BloodType.getValues().map((BloodType bloodType) {
              return DropdownMenuItem<BloodType>(
                value: bloodType,
                child: Text(
                  bloodType.toString(),
                  style: GoogleFonts.roboto(
                    textStyle: Theme.of(context).textTheme.headline4,
                    fontSize: 15,
                    fontWeight: FontWeight.w500,
                    color: Colors.black,
                  ),
                ),
              );
            }).toList()

Now you can call this widget from anywhere in your app and can be easly reused.

class MyPage extends StatefulWidget {
  @override
  _MyPageState createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> {

  BloodType currentSelectedValue;

  @override
  Widget build(BuildContext context) {
    return CustomDropDown(
      currentSelectedValue: this.currentSelectedValue,
      onChanged: (bloodType){
        setState((){
          this.currentSelectedValue = bloodType
        });
      },
    );
  }
}
answered on Stack Overflow Feb 6, 2021 by Ayad

User contributions licensed under CC BY-SA 3.0