mobx.dart icon indicating copy to clipboard operation
mobx.dart copied to clipboard

[Working solution included] Async @computed - How can I do heavy and async work inside a @computed?

Open fzyzcjy opened this issue 3 years ago • 8 comments

Hi thanks for the lib (that I have used extensively in my project - it is wonderful)! I have some @computed functions, which must do heavy and async work (e.g. run big computation which needs 0.5s and is async). Thus, I wonder how can I do it?

fzyzcjy avatar Oct 17 '21 06:10 fzyzcjy

computed lets you listen multiple observable's and derive a singular state when any on of the observable's gets changed. What do you mean by "@computed functions"?

benfgit avatar Oct 17 '21 15:10 benfgit

Well I mean just computed. ignore the function

fzyzcjy avatar Oct 17 '21 22:10 fzyzcjy

Mobx is more about reactive programming than async programming. You can have async actions to perform async work. Mobx focuses on the flow of the data in the app, using TFRP. Here is a good read about it. I hope this clears what Mobx is and is not.

benfgit avatar Oct 19 '21 06:10 benfgit

I agree. But the problem is, my @computed is so heavy that it takes, say, 0.5s, to finish. Thus I make it run in another thread (if you are interested - indeed run in Rust/C++ code) to make it async. The thought model is the same as sync @computed.

fzyzcjy avatar Oct 19 '21 09:10 fzyzcjy

I have come up with the solution:

import 'package:meta/meta.dart';
import 'package:mobx/mobx.dart';

part 'async_computed.g.dart';

@immutable
class NonReactiveTask<T> {
  final Future<T> Function() run;

  const NonReactiveTask(this.run);
}

class AsyncComputed<T> = _AsyncComputed<T> with _$AsyncComputed;

abstract class _AsyncComputed<T> with Store {
  /// If your dependency is stale: Please return null.
  /// Otherwise, please return a task that computes the value asynchronously.
  ///
  /// NOTE: **Requirement**: The returned task **should not** depend on any mobx reactive value!
  ///       Otherwise, it will NOT be reactive
  final NonReactiveTask<T>? Function() _fn;

  final String? name;

  _AsyncComputed(this._fn, T initValue, {this.name}) : _cache = initValue;

  @computed
  ObservableFuture<T>? get _future {
    final task = _fn();
    // print('[$name] call get _future task=$task');
    if (task == null) return null;
    return ObservableFuture(task.run());
  }

  T _cache;

  @computed
  bool get stale => _future == null || _future!.status != FutureStatus.fulfilled;

  @computed
  T? get freshData => stale ? null : maybeStaleData;

  @computed
  T get maybeStaleData {
    final futureFulfilledValue = _future?.value;
    if (futureFulfilledValue != null) _cache = futureFulfilledValue;

    return _cache;
  }
}

fzyzcjy avatar Oct 19 '21 09:10 fzyzcjy

The testing code (if you are interested):

// ignore_for_file: avoid_print

import 'package:common_dart/utils/async_computed.dart';
import 'package:mobx/mobx.dart';
import 'package:test/test.dart';

void main() {
  group('async_computed', () {
    test('simple', () async {
      // dependency graph:
      //     <-- a -->   <-- b
      //    /    |    \ /
      //   h <-- c --> d --> e
      //               |     |
      //               ----> f,g

      // expect to see:
      // 1. If [a] changes, [d] should not do heavy work immediately, but should do work after [c] finishes work
      // 2. If [a] changes, after [c] do work, [h] and [d] should do work *parallelly*
      // 3. If [a] changes, then [c], [d], [e] should do work one after one, and [f], [g] should change accordingly
      // 4. If [b] changes, only [d], [e] should do work, but [c], [h] should not do work

      final a = Observable(10, name: 'Observable_a');
      final b = Observable(10, name: 'Observable_b');

      final c = AsyncComputed(() {
        final aValue = a.value;
        return NonReactiveTask(() => _doHeavyWorkAndReturn('c', aValue * 2));
      }, 0, name: 'AsyncComputed_c');

      final d = AsyncComputed(() {
        final aValue = a.value;
        final bValue = b.value;
        final cFreshData = c.freshData;
        if (cFreshData == null) return null;

        return NonReactiveTask(() => _doHeavyWorkAndReturn('d', aValue * 2 + bValue * 2 + cFreshData * 2));
      }, 0, name: 'AsyncComputed_d');

      final h = AsyncComputed(() {
        final aValue = a.value;
        final cFreshData = c.freshData;
        if (cFreshData == null) return null;

        return NonReactiveTask(() => _doHeavyWorkAndReturn('h', aValue * 2 + cFreshData * 2));
      }, 0, name: 'AsyncComputed_h');

      final e = AsyncComputed(() {
        final dFreshData = d.freshData;
        if (dFreshData == null) return null;

        return NonReactiveTask(() => _doHeavyWorkAndReturn('e', dFreshData * 2));
      }, 0, name: 'AsyncComputed_e');

      final f = Computed(() => d.maybeStaleData * 2 + e.maybeStaleData * 2, name: 'Computed_f');
      final g = Computed(() => (d.freshData ?? 0) * 2 + (e.freshData ?? 0) * 2, name: 'Computed_g');

      autorun((_) => _log('[pseudo-UI] '
          'a=${a.value} '
          'b=${b.value} '
          'c=(${c.freshData}, ${c.maybeStaleData}) '
          'd=(${d.freshData}, ${d.maybeStaleData}) '
          'e=(${e.freshData}, ${e.maybeStaleData}) '
          'f=${f.value} '
          'g=${g.value} '
          'h=(${h.freshData}, ${h.maybeStaleData}) '));

      await Future<void>.delayed(const Duration(milliseconds: 500));

      _log('Before set a.value');
      Action(() => a.value = 100)();
      _log('After set a.value');

      await Future<void>.delayed(const Duration(milliseconds: 500));

      _log('Before set b.value');
      Action(() => b.value = 1000)();
      _log('After set b.value');

      await Future<void>.delayed(const Duration(milliseconds: 500));
    });
  });
}

Future<T> _doHeavyWorkAndReturn<T>(String name, T value) async {
  _log('$name start heavy work');
  await Future<void>.delayed(const Duration(milliseconds: 100));
  _log('$name end heavy work');
  return value;
}

void _log(String msg) => print('[${DateTime.now()}]\t$msg');

fzyzcjy avatar Oct 19 '21 09:10 fzyzcjy

To observe A and trigger async computations which produce B, you could observe A with an reaction and trigger async actions which updates B accordingly.

Ascenio avatar Dec 25 '21 20:12 Ascenio

@pavanpodila If this sounds good I will PR for it

fzyzcjy avatar Mar 18 '22 12:03 fzyzcjy