mobxjs / mobx.dart

MobX for the Dart language. Hassle-free, reactive state-management for your Dart and Flutter apps.
https://mobx.netlify.app
MIT License
2.39k stars 310 forks source link

Reacting to ObservableStream events via reaction (also through computed) #998

Closed saawhitelife closed 3 months ago

saawhitelife commented 3 months ago

The code below produces this output:

flutter: Timer tick computed
flutter: Timer tick listen
flutter: Timer tick listen
flutter: Timer tick listen
flutter: Timer tick listen

Reaction on stream value is not firing at all, I understand that it is just a raw value and not an observable returned by the reaction fn, however here the issue was closed with an advise to create a reaction just like that. Reaction on computed fires only once. Please let me know what am I doing wrong or if it is not possible by design. Thank you.

import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:provider/provider.dart';

part 'main.g.dart';

void main() {
  runApp(
    Provider(
      create: (context) => MyStore()..init(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    final store = context.read<MyStore>();

    return MaterialApp(
      title: 'Flutter Application',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
    );
  }
}

class MyStore = MyStoreBase with _$MyStore;

abstract class MyStoreBase with Store {
  final timerStream =
      ObservableStream(Stream.periodic(const Duration(seconds: 1)));

  final reactionDisposers = <ReactionDisposer>[];

  @computed
  int get tick {
    print('Timer tick computed');
    return timerStream.value;
  }

  void init() {
    timerStream.listen((value) {
      print('Timer tick listen');
    });

    reactionDisposers.add(reaction((_) => timerStream.value, (stream) {
      print('Timer tick reaction');
    }));

    reactionDisposers.add(reaction((_) => tick, (tick) {
      print('Timer tick reaction computed');
    }));
  }
}

mobx: 2.3.3.+2 flutter_mobx: 2.2.1+1 mobx_codegen: 2.6.1

amondnet commented 3 months ago

@saawhitelife https://mobx.netlify.app/guides/when-does-mobx-react#1-notifications-fire-when-an-observable-changes-value timerStream.value has never changed, so no reaction occurs.

  final timerStream =
      ObservableStream(Stream.periodic(const Duration(seconds: 1)));

value: null null null null null

  final timerStream =
      ObservableStream(Stream.periodic(const Duration(seconds: 1), (c) => c));

value: null 0 1 2 3 4

import 'package:fake_async/fake_async.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mobx/mobx.dart';

part 'simple_test.g.dart';

void main() {
  test("reaction test", () {
    // Any code run within [fakeAsync] is run within the context of the
    // [FakeAsync] object passed to the callback.
    fakeAsync((async) {
      // All asynchronous features that rely on timing are automatically
      // controlled by [fakeAsync].
      MyStore().init();
      // This will cause the timeout above to fire immediately, without waiting
      // 5 seconds of real time.
      async.elapse(const Duration(seconds: 5));
    });
  });
}

class MyStore = MyStoreBase with _$MyStore;

abstract class MyStoreBase with Store {
  final timerStream =
      ObservableStream(Stream.periodic(const Duration(seconds: 1), (c) => c));

  final reactionDisposers = <ReactionDisposer>[];

  @computed
  int get tick {
    print('Timer tick computed');
    return timerStream.value ?? 0;
  }

  void init() {
    timerStream.listen((value) {
      print('Timer tick listen');
    });

    reactionDisposers.add(reaction((_) => timerStream.value, (stream) {
      print('Timer tick reaction');
    }));

    reactionDisposers.add(reaction((_) => tick, (tick) {
      print('Timer tick reaction computed');
    }));
  }
}
Timer tick computed
Timer tick reaction
Timer tick computed
Timer tick listen
Timer tick reaction
Timer tick computed
Timer tick reaction computed
Timer tick listen
Timer tick reaction
Timer tick computed
Timer tick reaction computed
Timer tick listen
Timer tick reaction
Timer tick computed
Timer tick reaction computed
Timer tick listen
Timer tick reaction
Timer tick computed
Timer tick reaction computed
Timer tick listen
saawhitelife commented 3 months ago

@amondnet thank you for the explanation, it makes sense. Now I get where I did the mistake. I have actually tried to consider the case when the value is not being changed and added the equals callback to rule it out, but instead of hardcoding false there I did put true, yeah that was silly :) thanks