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 311 forks source link

`autorun` is not working as expected in the recent version of flutter. #897

Closed wiseaidev closed 9 months ago

wiseaidev commented 1 year ago

Hey everyone. I just wanted to give you an update on the previous issue. I found that the issue is reproducible on the current version of flutter, as shown in the following image:

112

However, if you try to change the lower bound of the SDK versions to 2.16.8, the function works as expected:

-   sdk: '>=2.18.6 <3.0.0'
+   sdk: '>=2.16.8 <3.0.0'

111

It seems like the widget's behavior has changed in the newer flutter version. In particular, it appears that the constructor has been updated in a way that causes this issue.

  const MyApp({Key? key}) : super(key: key);
  const MySplashPage({Key? key, required this.title}) : super(key: key);
  const MyApp({super.key});
  const MySplashPage({super.key, required this.title});

Have a look at the previous ticket for more details on this issue.

amondnet commented 1 year ago

@wiseaidev I tested with the version below.

  1. flutter 3.3.8: works
  2. flutter 3.3.10: works

    fvm use 3.3.10
    fvm flutter run
    💪 Running with sound null safety 💪
    
    An Observatory debugger and profiler on sdk gphone64 x86 64 is available at: http://127.0.0.1:57926/_bj4TiIsICo=/
    The Flutter DevTools debugger and profiler on sdk gphone64 x86 64 is available at: http://127.0.0.1:9100?uri=http://127.0.0.1:57926/_bj4TiIsICo=/
    E/SurfaceSyncer(13927): Failed to find sync for id=0
    W/Parcel  (13927): Expecting binder but got null!
    I/flutter (13927): Am action has been fired!
    I/flutter (13927): A reaction has been triggered! AppPage.home
    D/EGL_emulation(13927): app_time_stats: avg=1251.68ms min=84.12ms max=2419.23ms count=2
  3. flutter 3.7.0: works

I think you are mistaken because the reaction only happens once.

  1. AppState.currentPage = AppPage.splash;
  2. AppState.currentPage = AppPage.home; // reaction triggered
  3. AppState.currentPage = AppPage.home; // reaction not triggered
  4. AppState.currentPage = AppPage.home; // reaction not triggered

The reaction occurs only when the observable's value is a different value from the previous one.

amondnet commented 1 year ago
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:mobx_autorun_issue/store.dart';
import 'package:provider/provider.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(
    Provider(
      create: (_) => AppState()..initialize(),
      child: const MyApp(),
    ),
  );
}
var reactionTriggered = false;

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    // final appState = context.read<AppState>();
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: ReactionBuilder(
        builder: (context) {
          return reaction(
            (_) => context.read<AppState>().currentPage,
            (v) {
              print("A reaction has been triggered! $v");
              reactionTriggered = true;
            },
          );
        },
        child: Observer(
          name: "CurrentScreen",
          builder: (context) {
            switch (context.read<AppState>().currentPage) {
              case AppPage.splash:
                return const MySplashPage(title: "Splash Page");
              case AppPage.home:
                return const MySplashPage(title: "Home Page");
              case AppPage.login:
                return const Placeholder();
            }
          },
        ),
      ),
    );
  }
}

class MySplashPage extends StatefulWidget {
  const MySplashPage({super.key, required this.title});

  final String title;

  @override
  State<MySplashPage> createState() => _MySpashPageState();
}

class _MySpashPageState extends State<MySplashPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          print("Am action has been fired!");
          context.read<AppState>().goTo(
                AppPage.home,
              );
        },
        tooltip: 'Smash It.',
        child: const Icon(Icons.app_registration_outlined),
      ),
    );
  }
}
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.

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

import 'package:mobx_autorun_issue/main.dart';
import 'package:mobx_autorun_issue/store.dart';
import 'package:provider/provider.dart';

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(TestApp());

    expect(reactionTriggered, false);

    // Tap the '+' icon and trigger a frame.
    await tester.tap(find.byIcon(Icons.app_registration_outlined));
    await tester.pump();

    // Verify that our counter has incremented.
    expect(reactionTriggered, true);
  });
}

class TestApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (_) => AppState()..initialize(),
      child: const MyApp(),
    );
  }
}
fvm use 3.7.0
fvm flutter test
00:02 +0: Counter increments smoke test                                                                                                                                                                
Am action has been fired!
A reaction has been triggered! AppPage.home
00:02 +1: All tests passed! 
RossHS commented 1 year ago

@amondnet I'm sorry, I really don't understand what's wrong here 😂, but here are two code examples + video, in one case the reaction works, in the other it doesn't.

mobx store class -

import 'package:mobx/mobx.dart';

part 'reaction_builder_mobx.g.dart';

class Counter = CounterBase with _$Counter;

abstract class CounterBase with Store {
  @observable
  var value = 0;

  @action
  void increment() {
    value++;
  }

  @action
  void decrement() {
    value--;
  }
}

first code example (doesn`t work)

    return ReactionBuilder(
      builder: (context) => reaction(
        (_) => Provider.of<Counter>(context).value,
        (res) {
          log.d('current counter value - $res');
          if (res != 10) return;
          ScaffoldMessenger.of(context).showSnackBar(SnackBar(
            content: Text('current counter val - $res == 10!!!'),
          ));
        },
      ),
      child: .....

and the second one (everything is the same except for access to the provider)

    final counter = Provider.of<Counter>(context);
    return ReactionBuilder(
      builder: (context) => reaction(
        (_) => counter.value,
        (res) {
          log.d('current counter value - $res');
          if (res != 10) return;
          ScaffoldMessenger.of(context).showSnackBar(SnackBar(
            content: Text('current counter val - $res == 10!!!'),
          ));
        },
      ),
      child: ...

https://user-images.githubusercontent.com/30704653/215399112-47d79fbc-144a-450a-87a5-d0c5a02aec34.mp4

https://user-images.githubusercontent.com/30704653/215400382-e107c6a0-edb3-4b2b-aef5-3d0f857d9e7c.mp4

amondnet commented 1 year ago

@RossHS Provider.of<Counter>(context).value =>Provider.of<Counter>(context, listen: false).value or context.read<Counter>().value

https://github.com/rrousselGit/provider#reading-a-value

https://github.com/rrousselGit/provider/blob/ade6982f3298d151e59f8c605a475447ca7e7ac9/lib/src/provider.dart#L295

    return ReactionBuilder(
      builder: (context) => reaction(
        (_) => Provider.of<Counter>(context, listen: false).value,
        (res) {
          log.d('current counter value - $res');
          if (res != 10) return;
          ScaffoldMessenger.of(context).showSnackBar(SnackBar(
            content: Text('current counter val - $res == 10!!!'),
          ));
        },
      ),
      child: .....
RossHS commented 1 year ago

@amondnet You are right, thank you very much. All because of my inattention 🌚

wiseaidev commented 1 year ago

Hey folks,

Thanks for your contributions to the conversation. I tried both of your suggestions in the app I am currently building, but, unfortunately, it is still not working.

213124

The reaction occurs only when the observable's value is a different value from the previous one.

Exactly. But the thing is that it is not triggered for the first time. I think the first image I shared is somewhat misleading. However, I am not sure what is wrong, Mobx? Provider? Flutter? Gradle? Android studio? I literally have no idea. However, for now, I am going to downgrade flutter to the version that was working before.

I think that's enough flutter for the week cause this issue is so trippy. I will keep you updated on this matter. Have fun!