Home > Mobile >  Double ExpansionTile
Double ExpansionTile

Time:04-14

I want to write filters with categories and subcategories which can be in the form of a drop down list. I wrote this with an ExpansionTile, and put a Checkbox in the title of the ExpansionTile. And I ran into the problem that the Checkbox for subcategories works, but the Checkbox for the category does not work. Those. when you click on a category, subcategories open / close. But if you click on the Checkbox in the category, then nothing happens. Take a look.

enter image description here

How can I fix the problem so that the Checkbox in the category reacts to clicks.

    class _FilterDialogUserState extends State<FilterDialogUser> {
  Map<String, List<String>?> filters = {};

  @override
  void initState() {
    super.initState();
    filters = widget.initialState;
  }

  void _handleCheckFilter(bool checked, String key, String value) {
    final currentFilters = filters[key] ?? [];
    if (checked) {
      currentFilters.add(value);
    } else {
      currentFilters.remove(value);
    }
    setState(() {
      filters[key] = currentFilters;
    });
  }

  List<String> germany = ['Germany'];
  List<String> germanyCar = [
    'Audi',
    'BMW',
    'Volkswagen'
  ];

  @override
  Widget build(BuildContext context) {
    return SimpleDialog(
        title: const Text('Filters',
            textAlign: TextAlign.center,
            style: TextStyle(
              fontSize: 25,
              fontFamily: 'SuisseIntl',
            )),
        contentPadding: const EdgeInsets.all(16),

        children: [
          Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                ExpansionTile(
                  tilePadding: EdgeInsets.zero,
                  childrenPadding: EdgeInsets.symmetric(horizontal: 15),
                  title: CustomCheckboxTile(
                    value: filters['germany']?.contains(germany) ?? false,
                    onChange: (check) => _handleCheckFilter(
                        check, 'germany', germany.toString()),
                    label: germany
                        .toString()
                        .replaceAll("[", '')
                        .replaceAll("]", ''),
                  ),
                  initiallyExpanded: () {
                    for (final f in germanyCar) {
                      if (filters['germanyCar']?.contains(f) ?? false) {
                        return true;
                      }
                    }
                    return false;
                  }(),
                  children: germanyCar.map((e) {
                    return Row(children: [
                      CustomCheckboxTile(
                        value: filters['germanyCar']?.contains(e) ?? false,
                        onChange: (check) =>
                            _handleCheckFilter(check, 'germanyCar', e),
                        label: e,
                      ),
                    ]);
                  }).toList(),
                ),
                const SizedBox(
                  height: 5,
                ),
                ElevatedButton(
                    onPressed: () {
                      Navigator.of(context).pop();
                      widget.onApplyFilters(filters);
                    },
                    child: const Text('APPLY',
                        style: TextStyle(color: Colors.black)),
                    style: ButtonStyle(
                      backgroundColor: MaterialStateProperty.all(Colors.grey),
                    )),
                const SizedBox(
                  height: 5,
                ),
                ElevatedButton(
                    onPressed: () async {
                      setState(() {
                        filters.clear();
                      });
                      widget.onApplyFilters(filters);
                    },
                    child: const Text('RESET FILTERS',
                        style: TextStyle(color: Colors.black)),
                    style: ButtonStyle(
                      backgroundColor: MaterialStateProperty.all(Colors.grey),
                    )),
              ])
        ]);
  }
}

custom_checkbox_tile.dart

    class CustomCheckboxTile extends StatelessWidget {
  final String label;
  final bool value;
  final void Function(bool)? onChange;

  const CustomCheckboxTile({Key? key,
    required this.label, required this.value, this.onChange,}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Checkbox(
          visualDensity: VisualDensity.compact,
          value: value,
          onChanged: (_) {
            if(onChange != null) {
              onChange!(!value);
            }
          },
        ),
        Text(label),
      ],
    );
  }
}

CodePudding user response:

Here is a complete example. For me, it is easier and cleaner to handle large data with ValueNotifier than with setState. You can use this code or customize it how you want. Copy and paste to the DartPad to find your answer.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Checkbox Expansion Tile Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const Home(title: 'Checkbox Expansion Tile Demo'),
    );
  }
}

class Home extends StatefulWidget {
  final String title;

  const Home({Key? key, required this.title}) : super(key: key);

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

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    final controller = CountryController([
      Country(
        name: 'Germany',
        cars: [
          Car(name: 'Audi'),
          Car(name: 'BMW'),
          Car(name: 'Volkswagen'),
        ],
      ),
      Country(
        name: 'Sweden',
        cars: [
          Car(name: 'Koenigsegg'),
          Car(name: 'Polestar'),
          Car(name: 'Volvo'),
        ],
      ),
    ]);

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: ValueListenableBuilder<List<Country>>(
          valueListenable: controller,
          builder: (context, countries, _) => Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              for (Country country in countries)
                ExpansionTile(
                  title: Text(country.name),
                  leading: Checkbox(
                    value: country.isChecked,
                    onChanged: (value) {
                      //controller.checkCountry(country.name);
                      controller.checkAllByCountry(country.name, value);
                    },
                  ),
                  children: [
                    for (Car car in country.cars)
                      CustomCheckboxTile(
                        title: car.name,
                        value: car.isChecked,
                        onChanged: (value) {
                          controller.checkCar(car.name);
                        },
                      ),
                  ],
                ),
              TextButton(
                onPressed: () {
                  controller.checkAll(true);
                },
                child: const Text('CHECK ALL'),
              ),
              TextButton(
                onPressed: () {
                  controller.checkAll(false);
                },
                child: const Text('RESET ALL'),
              ),
              TextButton(
                onPressed: () {
                  print(countries);
                },
                child: const Text('PRINT ALL'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class CustomCheckboxTile extends ListTile {
  CustomCheckboxTile({
    Key? key,
    required String title,
    required bool value,
    double leftPadding = 28.0,
    ValueChanged<bool?>? onChanged,
  }) : super(
          key: key,
          title: Text(title),
          leading: Padding(
            padding: EdgeInsets.only(left: leftPadding),
            child: Checkbox(
              value: value,
              onChanged: onChanged,
            ),
          ),
        );
}

class Country {
  Country({
    required this.name,
    this.isChecked = false,
    this.cars = const [],
  });

  final String name;
  final List<Car> cars;
  bool isChecked;

  @override
  String toString() =>
      'Country(name: $name, isChecked: $isChecked, cars: $cars)';
}

class Car {
  Car({required this.name, this.isChecked = false});

  final String name;
  bool isChecked;

  @override
  String toString() => 'Car(name: $name, isChecked: $isChecked)';
}

class CountryController extends ValueNotifier<List<Country>> {
  CountryController(List<Country>? countries) : super(countries ?? const []);

  void checkCountry(String countryName) {
    value = [
      for (var country in value)
        if (country.name == countryName)
          Country(
            name: country.name,
            isChecked: country.isChecked = !country.isChecked,
            cars: country.cars,
          )
        else
          country
    ];
  }

  void checkCar(String carName) {
    value = [
      for (var country in value)
        Country(
          name: country.name,
          isChecked: country.isChecked,
          cars: [
            for (var car in country.cars)
              if (car.name == carName)
                Car(
                  name: car.name,
                  isChecked: car.isChecked = !car.isChecked,
                )
              else
                car
          ],
        ),
    ];
  }

  void checkAllByCountry(String countryName, bool? isChecked) {
    if (isChecked == null) return;

    value = [
      for (var country in value)
        if (country.name == countryName)
          Country(
            name: country.name,
            isChecked: isChecked,
            cars: [
              for (var car in country.cars)
                Car(
                  name: car.name,
                  isChecked: isChecked,
                ),
            ],
          )
        else
          country
    ];
  }

  void checkAll(bool? isChecked) {
    value = [
      for (var country in value)
        Country(
          name: country.name,
          isChecked: isChecked ?? false,
          cars: [
            for (var car in country.cars)
              Car(
                name: car.name,
                isChecked: isChecked ?? false,
              ),
          ],
        ),
    ];
  }
}

Update

Added check all and reset all buttons.

  • Related