I’m having some trouble trying to figure out what I’m doing wrong. I’m trying to do a filter feature in a list of products. Take OperationalInterruption as a product object. I’m using MVVM so it can be difficult for me to show it, but I’ll do it step by step from the user pov.
First, I have my ProductsScreenView
, there we’ll have all the products coming from the API. In this screen we have a filter button:
CustomScrollView(
controller: _scrollController,
slivers: [
DefaultAppBar(
title: l10n.productScreenStepLabel(viewModel.step),
onTapBack: viewModel.didPop,
subTitle: viewModel.departmentTitle,
actions: [
/// This button VVV
Padding(
padding: const EdgeInsets.only(right: 8),
child: IconButton(
onPressed: viewModel.didTapFilter,
icon: const SvgViewer(asset: AppAssets.icFilter),
),
)
],
),
Then, we’ll have in the view model:
class ProductsScreenViewModel {
...
late List<ProductFilter> _filters;
...
ProductsScreenViewModel(
required this...
required this...
required this...
required this...
) {
final unansweredProductsFilter = ProductFilter(
id: 1,
label: 'Show unanswered products',
condition: (product) {
return switch (_userKind) {
UserKind.gondola => product.hasGondola == null,
UserKind.deposit => product.hasStock == null,
UserKind.lossPrevention => product.checkLoss == null,
_ => product.hasGondola == null,
};
},
);
_filters = [unansweredProductsFilter];
}
@override
void didTapFilter() {
final initialFilters = List<ProductFilter>.from(_filters);
onTapFilter?.call(initialFilters);
}
...
So, basically, I am initalizing the filters to manipulate them in the future. This is the filters model:
class ProductFilter {
final int id;
final String label;
final bool Function(OperationalInterruption) condition;
bool isSelected;
ProductFilter({
required this.id,
required this.label,
required this.condition,
this.isSelected = false,
});
}
That method opens a bottom sheet in ProductsScreenView
:
viewModel.onTapFilter = (initialFilters) {
showCustomModalBottomSheet(
body: DefaultFiltersBottomSheet(filters: initialFilters),
onDismiss: (productFilters) {
final filters = productFilters as List<ProductFilter>?;
viewModel.didFinishFilterProducts(filters ?? initialFilters);
},
);
};
I’m showing my bottom sheet now because I guess it is the problem core: When I change the isSelected
variable from my initialFilters
using checkboxes, I’m changing the _filters
in my view model too:
class DefaultFiltersBottomSheet extends StatefulWidget {
final List<ProductFilter> filters;
final VoidCallback? onTapClose;
const DefaultFiltersBottomSheet({
required this.filters,
super.key,
this.onTapClose,
});
@override
State<DefaultFiltersBottomSheet> createState() => _DefaultFiltersBottomSheetState();
}
class _DefaultFiltersBottomSheetState extends State<DefaultFiltersBottomSheet> {
@override
Widget build(BuildContext context) {
final l10n = ServiceLocator.get<LocalizeProtocol>().l10n;
return Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
decoration: const BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(40)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
UnconstrainedBox(
child: Container(
height: 2,
width: 32,
decoration: BoxDecoration(
color: AppColors.black.withOpacity(0.2),
borderRadius: BorderRadius.circular(16),
),
),
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Text(
'Filters',
textAlign: TextAlign.center,
style: AppFonts.jakartaBold(20, AppColors.black),
),
),
...widget.filters.map((filter) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: GestureDetector(
onTap: () {
setState(() {
filter.isSelected = !filter.isSelected;
});
},
child: Row(
children: [
Checkbox(
value: filter.isSelected,
onChanged: (isSelected) {
setState(() {
filter.isSelected = isSelected ?? false;
});
},
),
Expanded(
child: Text(
filter.label,
style: AppFonts.jakartaRegular(16, AppColors.black),
),
),
],
),
),
);
}),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 24),
child: Row(
children: [
Expanded(
child: ElevatedButton(
style: AppThemes.transparentButtonStyle,
onPressed: widget.onTapClose ?? context.pop,
child: Text(
l10n.defaultCancelInputTitle,
style: AppFonts.jakartaRegular(14, AppColors.darkGreen),
),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
style: AppThemes.defaultButtonStyle,
onPressed: () => context.pop(widget.filters),
child: Text(
'Filter',
style: AppFonts.jakartaBold(14, AppColors.white),
),
),
),
],
),
),
],
),
);
}
}
So, when I call onDismiss
in my bottom sheet with the context.pop()
, which calls a filter method that eventually calls a notifyListeners(), it’s visible that the _filters
variable at my viewmodel is being manipulated in the setState
at the bottom sheet.
I tried using `List.from()` to copy the lists without using the same reference but unsuccessful.
I don’t know if it’s a normal behavior to Flutter but that’s not what I want so I would like to hear some workarounds about that situation. Thanks.