Disposed ChangeNotifier subclasses trigger - A `DataSource` was used after being disposed.

2,189 views
Skip to first unread message

Vasu Nagendra

unread,
Oct 29, 2018, 7:00:28 AM10/29/18
to Flutter Dev
I have a StatefulWidget which has a data source (a subclass of `ChangeNotifier`). In my case, one of these is an expensive data source. I am fine with the results of this datasource being discarded, if the user navigates away from the screen *after* invoking the datasource. Here is a very simple example -- 

```
class MyScreen extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return MyScreenState();
}
}

class MyScreenState extends State<StatefulWidget> {
MyDataSource dataSource = MyDataSource();

@override
void initState() {
super.initState();
dataSource.addListener(callback);
}

@override
void dispose() {
dataSource.removeListener(callback);
dataSource.dispose();
super.dispose();
}

void callback() {
print("Do Something with dataSource's state");
}

@override
Widget build(BuildContext context) {
return null;
}
}
```

My dataSource looks something like this 

```
class MyDataSource extends ChangeNotifier {

void performFetch(http.Client client) {
expensiveOperation();
notifyListeners();
}

void expensiveOperation() async {
await Future.delayed(Duration(seconds: 30));
}
}
```

If the user navigates away from the screen *after* listeners have been notified it works as expected. But if the user navigates away during the `expensiveOperation`, I believe the following things happen (?) 

1. The datasource is *not* null -- why is this the case?. Once `super.dispose()` is called, both `MyScreen` and `MyScreenState` are no longer valid (or so I thought)
2. The datasource triggers a notifyListeners()
3. However the listeners have been removed and it triggers a `A MyDataSource was used after being disposed`. 

Here is what I've tried already 

1. Adding datasource?.performFetch() in my screen itself. It didn't make any difference since the `datasource` is not null (in `MyScreenState`) 
2. Adding `this?.notifylisteners()` in the datasource itself -- that's also not null, so no effect there 

Couple of questions I am hoping to get clarity on : 

1. Am I trying to accomplish something that doesn't make sense in the context of flutter? 
2. Is there a better/cleaner way to accomplish this? .. 

The equivalent in Swift (which is what I am familiar with) -- would be to check an `optional` -- in this case `this?.notifyListeners()` should be sufficient, since that will be set to `nil`. 

Any pointers/guidance would be very much appreciated. 

Kris Giesing

unread,
Oct 29, 2018, 11:35:47 AM10/29/18
to vasuki....@gmail.com, flutt...@googlegroups.com
As I understand the documentation for ChangeNotifier, calling dispose() on it is intended to free up any resources, but it doesn't affect any references to the object (i.e. MyScreenState.dataSource is not set to null). There are lifecycle checks in subsequent ChangeNotifier methods that check to make sure the ChangeNotifier wasn't already disposed - I think those are the errors you're hitting.

MyDataSource should cancel any outstanding operations when dispose() is called, because otherwise those outstanding operations will hang on to references to methods in MyDataSource, and when those methods are invoked, the assert will trip. If the outstanding operations can't effectively be canceled then you may need to have a permanent object that brokers those operations, so that there's always something valid for the operations to come back to. The broker object can then discard the result if all the clients of the information have themselves been disposed. (The broker object might just be MyDataSource itself.)

Hope this helps,

- Kris

--
You received this message because you are subscribed to the Google Groups "Flutter Dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to flutter-dev...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Ian Hickson

unread,
Oct 29, 2018, 4:31:32 PM10/29/18
to Vasu Nagendra, Flutter Dev
Override dispose() in MyDataSource to cancel whatever work performFetch and expensiveOperation are doing, so that performFetch never actually calls notifyListeners.

(Side note: you don't need to remove the listener before calling dispose, dispose clears the listener list.)

On Mon, Oct 29, 2018 at 4:00 AM Vasu Nagendra <vasuki....@gmail.com> wrote:
--
You received this message because you are subscribed to the Google Groups "Flutter Dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to flutter-dev...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
--

--
Ian Hickson

😸

Vasu Nagendra

unread,
Oct 29, 2018, 7:19:54 PM10/29/18
to Flutter Dev
Ah Brilliant! So simple! 

Kris/Ian - Thank you for your help!

For posterity -- I didn't quite implement the "cancel operations in dispose" exactly as-is. I am trying to keep the `expensiveOperation` reasonably simple -- I did this instead (which changed the general behavior of the "datasource" itself) -- there were a bunch of classes that were implementing similar behavior. I did this in the base class.

```
@override
void dispose() {
_disposed = true;
super.dispose();
}

@override
void notifyListeners() {
if (!_disposed) {
super.notifyListeners();
}
}
```

Presumably for the next developer reading this `_dispose` is intended to convey, "don't modify it outside this particular class".. But we shall see. 

Thanks again Kris and Ian -- appreciate your help! 

Thanks!
--Vasu
Reply all
Reply to author
Forward
0 new messages