0

Original Answer

I'm using the Getx State Management on Flutter.

Simplifying as much as possible:

I build a GetxController to control my Page, and in this controller i have a StatefulWidget instance that evoque http requests.

class MyController extends GetxController {
  Player player;
}   

class Player extends StatefulWidget {
  PlayerState state;

  @override
  PlayerState createState() {
    state = PlayerState();
    return state;
  }
}

class PlayerState extends State<Player> {
  void methodName async() {
    futureRequest().then((data) {
      // when the error ocurrs
      setState(() {});
    });
  }
}

The problem occurs when the user closes the mobile page, triggering the controller's close method, before the end of the request.

That way, when setState is triggered, there is no more page instance and the error occurs.

I believe that the solution would be to interrupt all requests related to this GetxController and "delete" this instance of StatefulWidget at the moment the controller close method was called.

I don't know if this would be right, and if it's how to do it ..

==================================================================

Updated Answer

The main problem was that the async request in getDetails() method, return a response even after the controller is disposed, even using GetBuilder, and this response carried a url from a video that is started by the videoPlayerController (a video_player plugin instance).

So, the user is in another screen but keep listen to the video that is playing on background.

As a workaround and thinking in apply good practices to the code, i make a refactor to use only stateless widgets, following the GetX rules. I solved the problem, but i had to convert the Future's to Stream's

The binding is being created with Get.lazyPut() to perform dependencies injection:

class Binding implements Bindings {
  Get.lazyPut<PlayerController>(() {
    return PlayerController(videoRepository: VideoRepository(VideoProvider(Dio())));
  });
}

This binding is linked to the page router, based on GetX documentation.

class AppPages {
  static final routes = [
    GetPage(name: Routes.MyRoute, page: () => MyPage(), binding: MyBinding()),
  ];
}

To prevent the controller to make actions even before it is disposed, i have to created a Stream and cancel it on controller dispose.

class MyController extends GetxController {
  
  MyController({@required this.repository}) : assert(repository != null);
  StreamSubscription<bool> stream;
  // Instance of plugin video_player 
  VideoPlayerController videoPlayerController;

  @override
  void onClose() {
    if (streamGetVideo != null) streamGetVideo.cancel();
    super.onClose();
    if (videoPlayerController != null) videoPlayerController?.dispose();
  }

  // This is the method called by the user on screen
  void loadVideo() {
    stream = getDetails().asStream().listen((bool response) {
      // This code is canceled on onClose() method by the stream
      if (response) update();
    });
  }

  Future<bool> getDetails() async {
    return await repository.getDetails().then((data) async {
      videoPlayerController = VideoPlayerController.network(data);
      initFuture = videoPlayerController.initialize();
      await initFuture.whenComplete(() { return true; });
    });
  }
}

I think that Flutter/GetX should have a better way to do this, without these workarounds that i made. If anyone has a better approach or a hint, i'm open to suggestions.

Patrick Freitas
  • 681
  • 1
  • 5
  • 18

2 Answers2

0

One solution could be to wrap your setState with

    if(mounted){
          setState(() {});
    }
moulte
  • 521
  • 3
  • 5
0

GetBuilder + update()

In GetX using a GetBuilder with update() takes care of that lifecycle checking / handling so you don't have to do it.


Below is an example of a screen/route being closed prior to an HTTP call finishing & calling setState(), without an exception thrown.

(On the 2nd screen, click the Go Back! button fast to simulate an already disposed StatefulWidget.)


Below, an update() call is used to update the screen, instead of setState(), but they are the same in a GetBuilder. GetBuilder is (extends) a StatefulWidget.

GetBuilder adds listeners to the Controller you pass it, either through init: constructor arg or via the GetBuilder<Type> parameter if the Controller was initialized elsewhere/earlier.

That listener will be disposed if the StatefulWidget (i.e. GetBuilder) is disposed.

(See GetBuilder's dispose() function for some wizardry. While adding a listener, the returned value from adding that listener, is a function to dispose/unsubscribe from that listen. Pretty clever.)

So the GetBuilder/StatefulWidget will never have its update() / setState() called if that widget has been disposed because the listener for those calls has been disposed. So a slow returning HTTP call won't attempt to update/setState a widget that no longer exists in the widget tree.

import 'package:flutter/material.dart';
import 'package:get/get.dart';

class HttpX extends GetxController {

  String slowValue = 'loading...';

  @override
  void onInit() {
    slowCall();
  }

  /// Simulate a slow, long running HTTP call
  Future<void> slowCall() async {
    slowValue = 'Slow call STARTED!';
    print(slowValue);
    update(); // update the screen to show started message
    await Future.delayed(Duration(seconds: 5), () {
      slowValue = 'Slow call FINISHED!';
      print(slowValue);
      update(); // won't call setState() if GetBuilder is disposed
    });
  }
}

class GetXDisposePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GetX Dispose'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('awaiting http call to finish'),
            RaisedButton(
              child: Text('Go Call Page'),
              onPressed: () => Get.to(SlowCallPage()),
              // using Get.to ↑ requires GetMaterialApp in place of MaterialApp in MyApp
            )
          ],
        ),
      ),
    );
  }
}

class SlowCallPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GetX Dispose - Go Back!'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            GetBuilder<HttpX>(
              init: HttpX(), // fake slow http call starts on init
              builder: (hx) => Text(hx.slowValue),
            ),
            RaisedButton(
              child: Text('Go Back!'),
              onPressed: () => Get.back(),
            ),
          ],
        ),
      ),
    );
  }
}
Baker
  • 24,730
  • 11
  • 100
  • 106
  • After refactor my code to only use StatelessWidgets as your example, i think that i figure out the problem. My "HttpX" controller is put in memory using Get.lazyPut(HttpX) because i use dependencies injection and i also need to call this controller methods in another controller based on my architeture. When i put the "HttpX" in the memory, after the page was disposed, the controller still are active, resulting on the problem. Have you have any advices for i continue to use the dependecies injection and still dispose the async callback? – Patrick Freitas Mar 08 '21 at 13:19
  • Where are you using `Get.lazyPut`? Please copy/paste code so I can see the surrounding code and get a better picture of how it is being used. – Baker Mar 08 '21 at 18:42
  • I edit the main question to provide other details. I solved the problem, but i'm not sure that this resolution is the best option. Any doubts, i'm here to help. – Patrick Freitas Mar 09 '21 at 17:53