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 _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.