I have a screen, that draws multiple child widgets called EventCard. Each of this cards have a switch button that can be clicked and it will dispatch a redux action to fire a HTTP call in order to update the model in the database with one of the options ( switch button ).
After the http call is finished, we check the response from API and if everything is okay, we want to display the snackbar on the screen.
The problem occurs when we are Scaffold.of(buildContext).showSnackBar()
function, as the buildContext is already dismounted, so i get the error:
Looking up a deactivated widget's ancestor is unsafe. At this point the state of the widget's element tree is no longer stable. To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling inheritFromWidgetOfExactType() in the widget's didChangeDependencies() method.)
Here is the build function of my screen component:
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, EventScreenModel>(
model: EventScreenModel(),
builder: (BuildContext context, EventScreenModel model) {
final int childCount = model.events[model.currentRoleId] != null
? model.events[model.currentRoleId].length
: 0;
return Scaffold(
appBar: CustomAppBar(),
body: model.isLoading
? Center(
child: PulsingLogo(),
)
: Scrollbar(
child: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(
top: 24.0, left: 24.0, bottom: 10.0),
child: Text(
'Moje udalosti',
style: Theme.of(context).textTheme.headline,
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
Event.Event event =
model.events[model.currentRoleId][index];
String role;
model.roles.forEach((i, acc) {
acc.forEach((listOfRoles) {
if (listOfRoles.academy_id == event.academy_id) {
role = listOfRoles.role;
}
});
});
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0),
child: EventCard(
event: event,
currentRoleId: model.currentRoleId,
role: role,
),
);
}, childCount: childCount),
),
if (childCount == 0)
SliverToBoxAdapter(
child: Center(
child: Padding(
padding: const EdgeInsets.all(36.0),
child: Text('Nenašli sa žiadne udalosti.'),
),
),
),
SliverToBoxAdapter(
child: FractionallySizedBox(
widthFactor: 0.8,
child: RaisedButton(
onPressed: () {},
child: Text('Načítať dalšie'),
),
),
)
],
),
),
drawer: Menu(),
);
}
);
}
And here is the child EventCard component:
import 'package:academy_app/components/switch_button.dart';
import 'package:academy_app/main.dart';
import 'package:academy_app/screens/events_screen.dart';
import 'package:academy_app/state/event_state.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:academy_app/models/event.dart';
class EventCard extends StatelessWidget {
const EventCard(
{Key key,
@required this.event,
this.showAttendanceButton = true,
this.role,
this.currentRoleId})
: super(key: key);
final Event event;
final bool showAttendanceButton;
final String role;
final int currentRoleId;
bool get isGoing {
if (role != null && role != 'player')
return event.coaches != null
? event.coaches
.firstWhere((coach) => coach.id == currentRoleId)
.pivot
.participate
: false;
return event.players != null
? event.players
.firstWhere((player) => player.id == currentRoleId)
.pivot
.participate
: false;
}
_changeAttendance(context) {
store.dispatch(
SetEventAttendanceAction(
eventId: event.id,
isGoing: !isGoing,
buildContext: context,
roleId: currentRoleId),
);
}
String getDay(String date) => DateTime.parse(date).day.toString();
String getMonth(String date) =>
DateFormat('MMM').format(DateTime.parse(date)).toUpperCase();
String getTime(String date) =>
DateFormat('HH:mm').format(DateTime.parse(date));
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
if (event != null)
Card(
child: ListTile(
title: Text(
event.name,
style: Theme.of(context).textTheme.title,
),
subtitle: Text(
getTime(event.start_time) + '-' + getTime(event.end_time),
),
leading: Container(
margin: EdgeInsets.all(4.0),
padding: EdgeInsets.all(5.0),
decoration: BoxDecoration(
color: Colors.orange[400],
borderRadius: BorderRadius.circular(4.0),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
getDay(event.start_time),
style: TextStyle(color: Colors.white),
),
Text(
getMonth(event.end_time),
style: TextStyle(color: Colors.white),
),
],
),
),
trailing: AttendanceSwitchButton(
onPressed: () => _changeAttendance(context),
isGoing: isGoing,
small: true,
),
),
),
],
);
}
}
And finally, the redux action that is called on click, that is also the one where the error is thrown:
import 'dart:convert';
import 'package:academy_app/constants.dart';
import 'package:academy_app/models/event.dart';
import 'package:academy_app/models/role.dart';
import 'package:academy_app/models/serializers.dart';
import 'package:academy_app/state/roles_state.dart';
import 'package:academy_app/state/ui_state.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'app_state.dart';
class GetEventsAction extends BarrierAction {
final int roleId;
GetEventsAction({
@required this.roleId,
});
@override
Future<AppState> reduce() async {
try {
final Role role = RolesState.rolesMap(state)[roleId];
if (role == null) {
return state;
}
final response = await http.get(
UrlBuilder.uri('v1/events/${role.role}/${role.id}'),
headers: UrlBuilder.headers);
if (response.statusCode != 200) {
print(response.body);
return state;
}
List<Event> events =
List.from(json.decode(response.body)).map((dynamic value) {
return serializers.deserializeWith(Event.serializer, value);
}).toList();
events.sort((a, b) {
return DateTime.parse(b.updated_at)
.compareTo(DateTime.parse(a.updated_at));
});
return state.copy(events: {
...state.events,
role.id: events,
});
} catch (error) {
throw Exception('Failed to get trainings data from server: $error');
}
}
}
class SetEventAttendanceAction extends BarrierAction {
final int eventId;
final bool isGoing;
final BuildContext buildContext;
final int roleId;
SetEventAttendanceAction(
{@required this.eventId,
@required this.isGoing,
@required this.buildContext,
@required this.roleId});
@override
Future<AppState> reduce() async {
try {
final Role role = RolesState.rolesMap(state)[roleId];
final response = await http.post(
UrlBuilder.uri(isGoing
? 'v1/event/$eventId/${role.role}/$roleId/going'
: 'v1/event/$eventId/${role.role}/$roleId/not-going'),
headers: UrlBuilder.headers);
if (response.statusCode != 200) {
return state;
}
if (response.body == 'false') {
Scaffold.of(buildContext).showSnackBar(
SnackBar(
content: Text(
isGoing
? 'Nepodarilo sa prihlásiť na udalosť.'
: 'Nepodarilo sa odhlásiť z udalosti.',
//style: Theme.of(buildContext).textTheme.subtitle,
),
),
);
} else if (response.body == 'true') {
// store.dispatch(GetEventsAction(roleId: state.currentRoleId));
Scaffold.of(buildContext).showSnackBar(
SnackBar(
content: Text(
isGoing
? 'Boli ste prihlásený na udalosť.'
: 'Boli ste odhlásený z udalosti.',
//style: Theme.of(buildContext).textTheme.subtitle,
),
),
);
} else {
return state;
}
} catch (error) {
throw Exception('Failed to get training data from server: $error');
}
}
}
Any ideas?