Paul
Paul

Reputation: 197

Double ExpansionTile

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 FilterDialogUser extends StatefulWidget {
  final void Function(Map<String, List<String>?>) onApplyFilters;

  final Map<String, List<String>?> initialState;

  const FilterDialogUser({
    Key? key,
    required this.onApplyFilters,
    this.initialState = const {},
  }) : super(key: key);

  @override
  State<FilterDialogUser> createState() => _FilterDialogUserState();
}

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;
    });
  }


  final countries = [
    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'),
      ],
    ),
    Country(
      name: 'Russian',
      cars: [
        Car(name: 'GAZ'),
        Car(name: 'Lada'),
        Car(name: 'ZAZ'),
      ],
    ),
  ];

  @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: [
                for (Country country in countries)
                  ExpansionTile(
                      tilePadding: EdgeInsets.zero,

                      childrenPadding: EdgeInsets.symmetric(horizontal: 15),
                      title: CustomCheckboxTile(
                        value:

                            filters['country']?.contains(country.name) ?? false,
                        onChange: (check) =>
                            _handleCheckFilter(check, 'country', country.name),
                        label: country.name,
                      ),
                      initiallyExpanded: () {
                        for (final Car car in country.cars) {
                          if (filters['cars']?.contains(car.name) ?? false) {
                            return true;
                          }
                        }
                        return false;
                      }(),
                      children: [
                        for (Car car in country.cars)
                          CustomCheckboxTile(
                            value: filters['cars']?.contains(car.name) ?? false,
                            onChange: (check) =>
                                _handleCheckFilter(check, 'cars', car.name),
                            label: car.name,
                          )
                      ])
              ]),
          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),
              )),
        ]);
  }
}

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

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

  bool get isAllCarsChecked => cars.every((car) => car.isChecked);

  bool get isAllChecked => isAllCarsChecked && isChecked;

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

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

  final String name;
  final bool isChecked;



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

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),
      ],
    );
  }
}

Upvotes: 0

Views: 438

Answers (1)

Arnas
Arnas

Reputation: 832

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) {
    // Initial data. It can be loaded from the local or remote database.
    final countries = [
      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'),
        ],
      ),
      Country(
        name: 'Russian',
        cars: [
          Car(name: 'GAZ'),
          Car(name: 'Lada'),
          Car(name: 'ZAZ'),
        ],
      ),
    ];

    // It's simple Provider for passing value down in the widget tree.
    // And will keep you data until app will be killed or `Provider` will be removed from the widget tree.
    // If you use Provider package the you dont need it.
    return Provider(
      value: CountryController(countries), // Add to the controller.
      child: 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) {
    // Call value from the above widget tree.
    final controller = Provider.of<CountryController>(context);
    
    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.isAllCarsChecked,
                    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.cars = const [],
  });

  final String name;
  final List<Car> cars;

  bool get isAllCarsChecked => cars.every((car) => car.isChecked);

  @override
  String toString() {
    return 'Country(name: $name, cars: $cars)';
  }
}

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

  final String name;
  final 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,
            cars: country.cars,
          )
        else
          country
    ];
  }

  void checkCar(String carName) {
    value = [
      for (var country in value)
        Country(
          name: country.name,
          cars: [
            for (var car in country.cars)
              if (car.name == carName)
                Car(
                  name: car.name,
                  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,
            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,
          cars: [
            for (var car in country.cars)
              Car(
                name: car.name,
                isChecked: isChecked ?? false,
              ),
          ],
        ),
    ];
  }
}

Simple value provider

class Provider<T> extends StatelessWidget {
  const Provider({
    Key? key,
    required this.value,
    required this.child,
  }) : super(key: key);

  final T value;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return _InheritedWidget<T>(
      value: value,
      child: child,
    );
  }

  static T of<T>(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<_InheritedWidget<T>>()!
        .value;
  }
}

class _InheritedWidget<T> extends InheritedWidget {
  const _InheritedWidget({
    Key? key,
    required this.value,
    required Widget child,
  }) : super(key: key, child: child);

  final T value;

  @override
  bool updateShouldNotify(_InheritedWidget<T> oldWidget) {
    return oldWidget.value != value;
  }
}

Countries Inherited Widget

class CountriesProvider extends InheritedWidget {
  const CountriesProvider({
    Key? key,
    required this.countries,
    required Widget child,
  }) : super(key: key, child: child);

  final List<Country> countries;

  static List<Country> of(BuildContext context) {
    final result =
        context.dependOnInheritedWidgetOfExactType<CountriesProvider>();

    if (result == null) {
      debugPrint(
        'Returned empty countries list due to not found CountriesProvider in the above widget tree.',
      );
    }

    return result?.countries ?? [];
  }

  @override
  bool updateShouldNotify(CountriesProvider oldWidget) {
    return oldWidget.countries != countries;
  }
}

// Usage:
    //...
    return CountriesProvider(
      countries: countries, // Add countries list.
      child: MaterialApp(
    //...

    // Call countries from the above widget tree.
    final countries = CountriesProvider.of(context);
    // Add to the controller.
    final controller = CountryController(countries);

Update

Added check all and reset all buttons.

Upvotes: 1

Related Questions