GIfatahTH / states_rebuilder

a simple yet powerful state management technique for Flutter
494 stars 56 forks source link

Strange behavior with the new OnBuilder.createFuture #259

Closed tk2232 closed 2 years ago

tk2232 commented 2 years ago

I tested the new builder OnBuilder.createFuture and noticed a strange behavior.

The reactive model loses its state on complex objects.

Working example

import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:states_rebuilder/scr/state_management/rm.dart';

class DataObject {
  DataObject(this.data);

  final int data;
}

class Model {
  Model(this.stream);
  final Stream<DataObject> stream;
}

class TestingPage extends StatelessWidget {
  TestingPage({
    Key? key,
  }) : super(key: key);

  BehaviorSubject<DataObject> subject = BehaviorSubject();

  Future<Model> _getStreamWithObject() async {
    await Future.delayed(const Duration(milliseconds: 500));
    return Model(subject.stream);
  }

  Future<Stream<DataObject>> _getStream() async {
    await Future.delayed(const Duration(milliseconds: 500));
    return subject.stream;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            OnBuilder.createFuture(
              creator: () => _getStreamWithObject(),
              builder: (ReactiveModel<Model> rm) {
                return rm.onAll(
                  onWaiting: () => const CircularProgressIndicator(),
                  onError: (_, __) => const Text('Error'),
                  onData: (Model model) {
                    print(model);
                    if (model.stream == null) {
                      return const Text('Empty Stream');
                    } else {
                      return const Text('Passed');
                    }
                  },
                );
              },
            )
          ],
        ),
      ),
    );
  }
}

Not working

is in waiting state forever. In another case, the stream is even zero, but this example is unfortunately too complex to reproduce here

import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:states_rebuilder/scr/state_management/rm.dart';

class DataObject {
  DataObject(this.data);

  final int data;
}

class Model {
  Model(this.stream);
  final Stream<DataObject> stream;
}

class TestingPage extends StatelessWidget {
  TestingPage({
    Key? key,
  }) : super(key: key);

  BehaviorSubject<DataObject> subject = BehaviorSubject();

  Future<Model> _getStreamWithObject() async {
    await Future.delayed(const Duration(milliseconds: 500));
    return Model(subject.stream);
  }

  Future<Stream<DataObject>> _getStream() async {
    await Future.delayed(const Duration(milliseconds: 500));
    return subject.stream;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            OnBuilder.createFuture(
              creator: () => _getStream(),
              builder: (ReactiveModel<Stream<DataObject>> rm) {
                return rm.onAll(
                  onWaiting: () => const CircularProgressIndicator(),
                  onError: (_, __) => const Text('Error'),
                  onData: (Stream<DataObject> stream) {
                    print(stream);
                    if (stream == null) {
                      return const Text('Empty Stream');
                    } else {
                      return const Text('Passed');
                    }
                  },
                );
              },
            )
          ],
        ),
      ),
    );
  }
}
GIfatahTH commented 2 years ago

@tk2232 thank you for these edge cases issues. You are one of the best contributor. Here is the source of the bug

https://github.com/GIfatahTH/states_rebuilder/blob/e6d2054e677e8c68d73f3a5a6a8040c1e11b4ab3/states_rebuilder_package/lib/scr/state_management/reactive_model/reactive_model_imp.dart#L511

In states_rebuilder if a future resolves with a stream or on other function, the state will be mutated to listed to the stream.

All I have to do to fix the issue is to check that the state is not itself a stream.

Here is the fix:

subscription = stream.listen(
      (event) {
        if (event is T Function() || (event is Stream && this is! ReactiveModelImp<Stream<dynamic>>)) {
          // This is called from async read persisted state.
          // The state is read from local storage asynchronously. So as in
          // InjectedImpRedoPersistState.mockableCreator the creator return
          // a function after awaiting for the future.
          _snapState = _snapState
              .oldSnapState!; // Return to old state without notification.
          setStateNullable(
            (_) => event is T Function() ? event() : event,
            middleSetState: middleSetState,
          );
          return;
        }
        middleSetState(StateStatus.hasData, event);
        if (completer?.isCompleted == false) {
          completer!.complete();
        }
      },
tk2232 commented 2 years ago

Thank you for the quick solution. Ok something in the direction I thought. I will test it.

Of course I will try to find more bugs and suggest improvements. For me there is no better state management solution