Closed abbjetmus closed 8 months ago
Can you paste the full screen for context?
// Drawer
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:joinorcreate_app/constants.dart';
import 'package:signals/signals_flutter.dart';
final defaultEventFilters = {
'startDateTime': '',
'endDateTime': '',
'categories': [],
'eventTypes': [],
'minParticipants': '',
'maxParticipants': '',
'minAge': '',
'maxAge': ''
};
final eventFilters = mapSignal(defaultEventFilters);
final eventFiltersChanged =
computed(() => eventFilters.value != defaultEventFilters);
final Signal<String?> _filterSportCategory = signal(null);
final Signal<String?> _filterEventType = signal(null);
Signal<int?> _filterMinParticipants = signal(null);
Signal<int?> _filterMaxParticipants = signal(null);
Signal<int?> _filterMinAge = signal(null);
Signal<int?> _filterMaxAge = signal(null);
class EventFilterDrawer extends StatefulWidget {
const EventFilterDrawer({super.key});
@override
State<EventFilterDrawer> createState() => _EventFilterDrawerState();
}
class _EventFilterDrawerState extends State<EventFilterDrawer>
with SingleTickerProviderStateMixin {
final Signal<int> _currentStep = signal(0);
late TabController _tabController;
final _minParticipantsController = TextEditingController();
final _maxParticipantsController = TextEditingController();
final _minAgeController = TextEditingController();
final _maxAgeController = TextEditingController();
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_tabController.addListener(() {
_currentStep.value = _tabController.index;
});
_minParticipantsController.text =
eventFilters['minParticipants'].toString();
_maxParticipantsController.text =
eventFilters['maxParticipants'].toString();
_minAgeController.text = eventFilters['minAge'].toString();
_maxAgeController.text = eventFilters['maxAge'].toString();
}
@override
Widget build(BuildContext context) {
double height = MediaQuery.of(context).size.height;
return Drawer(
child: ListView(children: [
SizedBox(
height: height,
child: Watch((context) {
return Column(children: [
TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Event Info'),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: <Widget>[
_buildStep2(),
],
),
),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: SizedBox(
height: 50,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
backgroundColor: Colors.white,
padding: EdgeInsets.all(0),
foregroundColor: jocPrimary),
onPressed: () {
eventFilters.value = defaultEventFilters;
Navigator.of(context).pop();
},
child: Text('Återställ')),
)),
Expanded(
child: SizedBox(
height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
padding: EdgeInsets.all(0),
backgroundColor: jocPrimary,
),
onPressed: () {
batch(() {
eventFilters['minParticipants'] =
_minParticipantsController.text;
eventFilters['maxParticipants'] =
_maxParticipantsController.text;
eventFilters['minAge'] = _minAgeController.text;
eventFilters['maxAge'] = _maxAgeController.text;
});
Navigator.of(context).pop();
},
child: Text('Applicera')),
))
],
)
]);
}))
]));
}
_buildStep2() {
return Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
DropdownButtonFormField(
value: _filterSportCategory.value,
decoration: InputDecoration(
labelText: 'Sport Category',
border: OutlineInputBorder(),
),
items: ['Football', 'Basketball', 'Tennis', 'Cricket']
.map((label) => DropdownMenuItem(
value: label,
child: Text(label),
))
.toList(),
onChanged: (value) {
_filterSportCategory.value = value;
},
),
SizedBox(
height: 16,
),
DropdownButtonFormField(
value: _filterEventType.value,
decoration: InputDecoration(
labelText: 'Event Type',
border: OutlineInputBorder(),
),
items: ['Match', 'Training', 'Tryout']
.map((label) => DropdownMenuItem(
value: label,
child: Text(label),
))
.toList(),
onChanged: (value) {
_filterEventType.value = value;
},
),
Padding(
padding: EdgeInsets.only(top: 16),
child: Align(
alignment: Alignment.centerLeft,
child: Text('Antal deltagare'))),
Row(
children: <Widget>[
Flexible(
child: Container(
padding: const EdgeInsets.only(right: 8, top: 8, bottom: 8),
child: TextFormField(
decoration: InputDecoration(
labelText: 'Min',
border: OutlineInputBorder(),
),
controller: _minParticipantsController,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
],
onChanged: (value) {
_filterMinParticipants.value = int.tryParse(value);
},
),
),
),
Flexible(
child: Container(
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: TextFormField(
decoration: InputDecoration(
labelText: 'Max',
border: OutlineInputBorder(),
),
controller: _maxParticipantsController,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
],
onChanged: (value) {
_filterMaxParticipants.value = int.tryParse(value);
},
),
),
),
],
),
Padding(
padding: EdgeInsets.only(top: 16),
child:
Align(alignment: Alignment.centerLeft, child: Text('Ålder'))),
Row(
children: <Widget>[
Flexible(
child: Container(
padding: const EdgeInsets.only(right: 8, top: 8, bottom: 8),
child: TextFormField(
decoration: InputDecoration(
labelText: 'Min',
border: OutlineInputBorder(),
),
controller: _minAgeController,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
],
onChanged: (value) {
_filterMinAge.value = int.tryParse(value);
},
),
),
),
Flexible(
child: Container(
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: TextFormField(
controller: _maxAgeController,
decoration: InputDecoration(
labelText: 'Max',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
],
onChanged: (value) {
_filterMaxAge.value = int.tryParse(value);
},
),
),
),
],
),
],
),
);
}
}
// Other screen using signal
import 'package:flutter/material.dart';
import 'package:joinorcreate_app/constants.dart';
import 'package:joinorcreate_app/main.dart';
import 'package:joinorcreate_app/src/app_drawer.dart';
import 'package:joinorcreate_app/src/events/events_list_screen.dart';
import 'package:joinorcreate_app/src/events/events_map_screen.dart';
import 'package:joinorcreate_app/src/events/widgets/event_filter_drawer.dart';
import 'package:signals/signals_flutter.dart';
class EventTabsScreen extends StatefulWidget {
const EventTabsScreen({Key? key}) : super(key: key);
static const routeName = 'event-tabs';
@override
State<EventTabsScreen> createState() => _EventTabsScreenState();
}
class _EventTabsScreenState extends State<EventTabsScreen>
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
}
@override
bool get wantKeepAlive => true; // Prevents screens from rebuilding
@override
Widget build(BuildContext context) {
super.build(context); // Required for AutomaticKeepAliveClientMixin
return Scaffold(
appBar: AppBar(
title: const Text('Event'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'lista'),
Tab(text: 'karta'),
Tab(text: 'mina event'),
Tab(text: 'konversationer'),
],
),
actions: [
Builder(
builder: (context) => Stack(
children: <Widget>[
IconButton(
icon: Icon(
Icons.manage_search,
size: 26,
),
onPressed: () => Scaffold.of(context).openEndDrawer(),
),
Watch((context) {
if (eventFilters.value != defaultEventFilters) {
return Positioned(
right: 13,
top: 16,
child: Container(
padding: EdgeInsets.all(1),
decoration: BoxDecoration(
color: jocPrimary,
borderRadius: BorderRadius.circular(6),
),
constraints: BoxConstraints(
minWidth: 8,
minHeight: 8,
),
),
);
}
return Container();
})
],
),
)
]),
drawer: const AppDrawer(),
endDrawer: EventFilterDrawer(),
body: TabBarView(
controller: _tabController,
physics: NeverScrollableScrollPhysics(),
children: const [
// Replace with your custom widgets for each tab
EventsListScreen(),
EventsMapScreen(),
Center(child: Text('Tab 3 Content')),
Center(child: Text('Tab 4 Content')),
],
),
);
}
}
MapSignals use the same instance so the computed will always be false. You're comparing it to itself.
eventFilters.value != defaultEventFilters // always false
test('map signal', () {
final defaultEventFilters = {
'startDateTime': '',
'endDateTime': '',
'categories': [],
'eventTypes': [],
'minParticipants': '',
'maxParticipants': '',
'minAge': '',
'maxAge': ''
};
final eventFilters = mapSignal(defaultEventFilters);
final eventFiltersChanged = computed(
() => eventFilters.value != defaultEventFilters,
);
expect(eventFiltersChanged.value, false);
eventFilters['minAge'] = '50';
// still false even though the value changed
expect(eventFiltersChanged.value, false);
});
It doesn't look like you even need the computed/comparison in your case...Why not just watch the map signal directly? It sends a notification when updated so the widgets should rebuild.
@jinyus your correct, this works. Compared maps in the wrong way. Would be nice if there are examples in the documentation where concrete object types are used instead of maps if that is possible?
Thanks guys!
final defaultEventFilters = {
'startDateTime': '',
'endDateTime': '',
'categories': [],
'eventTypes': [],
'minParticipants': '',
'maxParticipants': '',
'minAge': '',
'maxAge': ''
};
final eventFilters = mapSignal({
'startDateTime': '',
'endDateTime': '',
'categories': [],
'eventTypes': [],
'minParticipants': '',
'maxParticipants': '',
'minAge': '',
'maxAge': ''
});
final eventFiltersChanged = computed(() =>
!DeepCollectionEquality().equals(eventFilters.value, defaultEventFilters));
You could reduce code duplication:
final eventFilters = mapSignal(Map.from(defaultEventFilters));
Another minor improvement is to instantiate the Equality object outside of the closure. You're creating a new instance everytime which gives the GC extra work.
final deepEq = DeepCollectionEquality();
final eventFiltersChanged = computed(() =>
!deepEq.equals(eventFilters.value, defaultEventFilters));
I have this code at the top of a drawer:
Then when tapping a button I update the eventFilters:
This changes the signal and closes the drawer.
Then in another screen I have:
But this doesn't rebuild that part even though the condition is correct. Doesn't work either when using the computed. What am I doing wrong here is it that the signals in the drawer class are not global although it's defined at the top of the file?
That doesn't seem to be the case either. Not sure what the issue is here, kind of trivial example?