'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.
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),
],
);
}
}
Solution 1:[1]
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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |

