3

I have isolate that makes some heavy calculations then on receive the list with the result run a for loop to add them to observable list with items var items = [].obs;

The thing is I'm trying to observe the items list from a splash controller and once the list != [] I'll navigate to another screen, so in onInit() I have this code:

class SplashController extends GetxController {
  @override
  void onInit() {
    final ItemsController _itemsController = Get.put(ItemsController());

    // TODO: implement onInit
    super.onInit();
    ever(_itemsController.items, (newItems) {
      print('new items here $newItems');
    });
  }
}

Despite the itemsController.items is populated (after the for loop I print the itemsController.items and it's not empty) the worker on the splash controller doesn't trigger when the items are added.

What am I doing wrong here? Is this the correct way to observe variable outside of widget using Getx? Can anyone help me with this, please?

Edit: In the items controller I’m adding the items this way

add(item) => items.add(item)
i6x86
  • 1,557
  • 6
  • 23
  • 42
  • Perhaps you could post the code for ItemsController so we can double check how newItems are added to the items observable. – Baker Jan 02 '21 at 02:07
  • @Baker ok I’ve edited the question. – i6x86 Jan 02 '21 at 02:11
  • @Baker Anyway I don’t think that the problem is in the items controller. I’m debugging the code after the isolate is terminated and there are the items added, but have no idea how to access them. – i6x86 Jan 02 '21 at 02:14
  • Maybe this snippet (not mine) helps for the data transport out of your isolate back into the main thread: https://gist.github.com/jebright/a7086adc305615aa3a655c6d8bd90264 – Baker Jan 02 '21 at 02:28
  • It’s the one I’ve implemented already :) when I transfer the data I run a for loop to populate the items list and it works like a charm. That’s why I think the problem should be in the way I observe the items in the splash controller. – i6x86 Jan 02 '21 at 02:35

2 Answers2

6

Continuing with the Isolate example, but without using a StatefulWidget i.e. no setState usage.

The ever worker in SplashX will receive items generated from the Isolate. The Stateless Widget page will display the latest item emitted from the Isolate.

SplashController + ever worker

class SplashX extends GetxController {
  ItemsX itemsX;

  SplashX({this.itemsX});

  @override
  void onInit() {
    super.onInit();

    ever(itemsX.items, (items) => print('Ever items: $items'));
  }
}

Items Controller

class ItemsX extends GetxController {
  RxList<String> items = RxList<String>();
  bool running = false;

  void add(String item) {
    items.add(item);
  }

  void updateStatus(bool isRunning) {
    running = isRunning;
    update();
  }

  void reset() {
    items.clear();
  }

  /// Only relevant for UnusedControllerPage
  List<Widget> get texts => items.map((item) => Text('$item')).toList();
}

Isolate Controller

class IsolateX extends GetxController {
  IsolateX({this.itemsX});

  ItemsX itemsX;
  Isolate _isolate;
  static int _counter = 0;
  ReceivePort _receivePort;
  bool running = false;

  static void _checkTimer(SendPort sendPort) async {
    Timer.periodic(Duration(seconds: 1), (Timer t) {
      _counter++;
      String msg = 'notification ' + _counter.toString();
      print('SEND: ' + msg);
      sendPort.send(msg);
    });
  }

  void _handleMessage(dynamic data) {
    itemsX.add(data); // update observable
  }

  void updateStatus(bool isRunning) {
    running = isRunning;
    update();
  }

  void start() async {
    itemsX.reset();
    updateStatus(true);
    _receivePort = ReceivePort();
    _isolate = await Isolate.spawn(_checkTimer, _receivePort.sendPort);
    _receivePort.listen(_handleMessage, onDone:() {
      print("done!");
    });
  }

  void stop() {
    if (_isolate != null) {
      updateStatus(false);
      _receivePort.close();
      _isolate.kill(priority: Isolate.immediate);
      _isolate = null;
    }
  }
}

Stateless Page

class MyHomePageStateless extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    ItemsX ix = Get.put(ItemsX()); // Instantiate ItemsController
    IsolateX isox = Get.put(IsolateX(itemsX: ix));
    SplashX sx = Get.put(SplashX(itemsX: ix));

    return Scaffold(
      appBar: AppBar(
        title: Text('Isolate Stateless'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            GetX<ItemsX>(
              builder: (ix) => Text(ix.items.isNotEmpty ? ix.items.last : ''),
            ),
          ],
        ),
      ),
      floatingActionButton: GetBuilder<IsolateX>(
        builder: (_ix) => FloatingActionButton(
          onPressed: _ix.running ? isox.stop : isox.start,
          tooltip: _ix.running ? 'Timer stop' : 'Timer start',
          child: _ix.running ? Icon(Icons.stop) : Icon(Icons.play_arrow),
        ),
      ),
    );
  }
}
Baker
  • 24,730
  • 11
  • 100
  • 106
  • Ok it might take a while, because I'm trying to implement it the way I need, but still your example works so i'll accept the answer. The flow I need is 1.Home -> button I call SplashScreen -> the splash screen controller call a function that runs Isolate onInit and then start worker to listen for changes. When the Isolate is done it adds the items from the onMessageHandle and then when stops I need to navigate to another screen. If you wish you can post another solution. – i6x86 Jan 02 '21 at 20:57
  • 1
    Another idea for you: use a Bindings class prior to `runApp` to start your Isolate process & also instantiate a controller with worker to listen for eventual Isolate data. Here's the basics of using Bindings, both blocking and non-blocking https://stackoverflow.com/a/64841379/2301224 – Baker Jan 02 '21 at 21:18
1

Here's two controllers, with one ever worker listening for events of another controller, where that controller's events are coming from data generated in an Isolate.

I'm not aware of anything special about generating data in an Isolate as opposed to any other async data source, but I'm not overly familiar with Isolates.

Controllers

class SplashX extends GetxController {
  ItemsX itemsX;

  SplashX({this.itemsX});

  @override
  void onInit() {
    super.onInit();

    ever(itemsX.items, (items) => print('Received items: $items'));
  }
}

class ItemsX extends GetxController {
  RxList<String> items = RxList<String>();

  void add(String item) {
    items.add(item);
  }

  /// Only relevant for SimplePage at bottom
  List<Widget> get texts => items.map((item) => Text('$item')).toList();
}

Page /w Isolate

And here's the edits to the Isolate snippet which you're using. I've instantiated ItemsX controller as a field and SplashX in onInit. (There shouldn't be a need to use Stateful Widgets since you can put all state into a Controller, but I didn't want to rewrite the Isolate example).

class _MyHomePageState extends State<MyHomePage> {
  Isolate _isolate;
  bool _running = false;
  static int _counter = 0;
  String notification = "";
  ReceivePort _receivePort;
  ItemsX ix = Get.put(ItemsX()); // Instantiate ItemsController

  @override
  void initState() {
    super.initState();
    SplashX sx = Get.put(SplashX(itemsX: ix));
    // ↑ Instantiate SplashCont with ever worker
  }

Change to the _handleMessage method:

  void _handleMessage(dynamic data) {
    //print('RECEIVED: ' + data);

    ix.add(data); // update observable

    setState(() {
      notification = data;
    });
  }

And finally the debug output results showing ever worker handling observable events (Received items...) :

[GETX] "ItemsX" has been initialized
[GETX] "SplashX" has been initialized
I/flutter (19012): SEND: notification 1
I/flutter (19012): Received items: [notification 1]
I/flutter (19012): SEND: notification 2
I/flutter (19012): Received items: [notification 1, notification 2]
I/flutter (19012): SEND: notification 3
I/flutter (19012): Received items: [notification 1, notification 2, notification 3]
I/flutter (19012): done!

Controllers in Non-Isolate Page

Example of using the same controllers above, without the noise of a Stateful Widget page and all the Isolate stuff.

class SplashX extends GetxController {
  ItemsX itemsX;

  SplashX({this.itemsX});

  @override
  void onInit() {
    super.onInit();

    ever(itemsX.items, (items) => print('Received items: $items'));
  }
}

class ItemsX extends GetxController {
  RxList<String> items = RxList<String>();

  void add(String item) {
    items.add(item);
  }

  /// Only relevant for SimplePage
  List<Widget> get texts => items.map((item) => Text('$item')).toList();
}

class SimplePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    ItemsX ix = Get.put(ItemsX());
    SplashX sx = Get.put(SplashX(itemsX: ix));

    return Scaffold(
      body: SafeArea(
        child: Column(
          children: [
            Expanded(
              flex: 10,
              child: Obx(
                    () => ListView(
                  children: ix.texts,
                ),
              ),
            ),
            Expanded(
              flex: 1,
              child: RaisedButton(
                child: Text('Add'),
                onPressed: () => ix.add('more...'),
              )
            )
          ],
        ),
      ),
    );
  }
}
Baker
  • 24,730
  • 11
  • 100
  • 106
  • Thanks for the answer, but it doesn't work for me. In the example with the statefull widget it does the trick, because you can setState data is received. If I do it this way it will work, but I need to implement it the way I posted, with worker listening on the splash controller and there's something wrong with this. Why am I so sure about it? Because after isolate is done with the calculations I run Future.delayed 30 sec on the splash and when check the Items are there. So the problem must be in the way I'm using the worker. – i6x86 Jan 02 '21 at 16:15
  • Now, on the question why I need to run Isolate? I have a function that runs some heavy calculations and when I've tried to present some type of Indicator, it doesn't appear until the function is done. I't was asynchronous but despite that, like I was said async doesn't mean concurrent, so the indicator appears only when the calculations were made, which is useless on so many levels. – i6x86 Jan 02 '21 at 16:27
  • Re: 1st comment, added another answer, without using StatefulWidget, i.e. no setState (which didn't impact the items observable / ever worker in the above example, only rebuilt the visual page). Controllers aren't impacted by widget rebuilds, their lifecycle is separate and handled by GetX. – Baker Jan 02 '21 at 20:04