Reputation: 197
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),
],
);
}
}
Upvotes: 0
Views: 438
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,
),
],
),
];
}
}
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;
}
}
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);
Added check all and reset all buttons.
Upvotes: 1