1

In my Flutter App, there are 2 screens called "home screen" and "data screen". In data screen I am making a GET Http request to a server and after if request is successful, I need to change some state using riverpod providers.

Data screen http request and provider code:

await Future.delayed(const Duration(seconds: 3));

var jsonBody = await HttpService.httpService.getRequest(
   path: Uri.parse("$address/getData"), 
   requestHeaders: {}
);

if((jsonBody["status"] as String).compareTo("OK") == 0 && mounted){       
   ref.read(dataProvider.notifier).setData(jsonBody["data"]);  
}

Here, if user closes the data screen (pops it off from nav stack) while http request is loading, the context of data screen widget is destroyed, so flutter gives me an error and says "looking up for a deactivated widget's ancestor is unsafe"

E/flutter (11326): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe.
E/flutter (11326): At this point the state of the widget's element tree is no longer stable.
E/flutter (11326): To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.
E/flutter (11326): #0      Element._debugCheckStateIsActiveForAncestorLookup.<anonymous closure> (package:flutter/src/widgets/framework.dart:4347:9)
E/flutter (11326): #1      Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:4361:6)
E/flutter (11326): #2      Element.getElementForInheritedWidgetOfExactType (package:flutter/src/widgets/framework.dart:4396:12)
E/flutter (11326): #3      ProviderScope.containerOf (package:flutter_riverpod/src/framework.dart:102:12)
E/flutter (11326): #4      ConsumerStatefulElement.read (package:flutter_riverpod/src/consumer.dart:613:26)
E/flutter (11326): #5      _DataScreenState.build.<anonymous closure> (package:flt_playground/screens/screen_data.dart:481:41)
E/flutter (11326): <asynchronous suspension>

To prevent this I also check if widget is mounted in the if statement. But the service takes my http request and I definitely need to update the state by calling that ref.read method.

How I can best handle this scenario? Should I prevent user closing the page while request is loading? What is the best practice for this use case?

Thank you for your answers and time..

Emre Turan
  • 83
  • 1
  • 8

2 Answers2

1

I am no Flutter expert, but I believe that your code should work. You check if the widget is still mounted with mounted boolean but somehow you still get an error. The best way to approach this issue (in my opinion) is to check other parts of your code, it might not be caused by that line.

Or simply, you can prevent the user from any interactions while the request is still happening. That would be my first go-to.

ElevatedButton(
  onPressed: isRequestInProgress ? null : popView,
  child: Text('Go Back'),
)

Maybe showing a loading indicator, even though they're annoying.

if (isRequestInProgress) {
  return Center(
    child: CircularProgressIndicator(),
  );
}

If you don't want to implement any of these, we can turn our attention to the error message:

To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

The error message suggest that you should save a reference to the ancestor widget in the didChangeDependencies() method if you need to safely refer to it within the dispose() method of your widget. We can use the dispose() method to handle the request cancelation, or anything else you want in that matter.

I can't see your whole code, so I will just give a simple example to show you how I would implement those methods. I will use DataProvider.

DataProvider dataProvider;

@override
void didChangeDependencies() {
  super.didChangeDependencies();
  dataProvider = Provider.of<DataProvider>(context, listen: false); // ancestor of DataProvider is Provider
}

Replace the DataProvider with your actual widget.

@override
void dispose() {
  // Use the dataProvider reference here
  dataProvider.cancelSomething()

  super.dispose();
}

I hope that helps your issue. Also here is a really cool link that might be a good starting point if nothing works for you.

leopanic13
  • 62
  • 5
  • Thank you for your answer, using didChangeDependencies and dispose methods may come in handy in different parts, but you got me wrong in one point. Checking the "mounted" attribute in if statement solves the error, I am not getting any errors. But the app does not work as I want it to. Once the Http request is sent to server, server confirms the process but if user closes the page, I can not update the state. So the app and service become desynced. I see you suggested to use progress indicator or just disable the button when request is loading. Is there no other way to handle this scenario? – Emre Turan Jul 14 '23 at 10:34
  • 1
    Totally got you a little wrong, sorry about that. Another way of handling the situation instead of progress indicator etc. could be using the `.then()` functionality. After your `getRequest()` method, you can call the the function: `getRequest().then(response){}`. After curly braces, you can save your desired state like that: `ref.read(dataProvider).state = response.data;`. You are using Riverpod so you might have tried that but just in case you haven't, I suggest you try that. Here is the [docs](https://api.flutter.dev/flutter/dart-async/Future/then.html) Edit: Hope that works out – leopanic13 Jul 14 '23 at 16:24
  • I'll give it a try, thank you so much – Emre Turan Jul 14 '23 at 18:08
1

As the error states, the problem is calling ref.read after an asynchronous computation. Since the user can pop the page, ref.read will cause the error.

If all you're trying to do is make sure the provider's method is executed, then consider storing a reference before any asynchronous work takes place.

Future<void> _computation() async {
  /// Obtain a reference to your provider
  final provider = ref.read(dataProvider.notifier);

  /// Mock HTTP request.
  await Future.delayed(const Duration(seconds: 3));

  /// Mock data.
  final jsonBody = {
    "status": "OK",
    "data": 1,
  };
  
  /// Note that you no longer have to check [mounted].
  if ((jsonBody["status"] as String).compareTo("OK") == 0) {
    provider.state.setData(jsonBody["data"] as int);
  }
}

After 3 seconds, the setData method of your provider will be called.

offworldwelcome
  • 1,314
  • 5
  • 11