marcglasberg / async_redux

Flutter Package: A Redux version tailored for Flutter, which is easy to learn, to use, to test, and has no boilerplate. Allows for both sync and async reducers.
Other
226 stars 40 forks source link

isWaiting(actionOrTypeOrList) does not rebuild view models #155

Open kuhnroyal opened 2 months ago

kuhnroyal commented 2 months ago

I migrated from some custom waits to the new isWaiting(actionOrTypeOrList).

While context.isWaiting does rebuild the widget, using isWaiting(actionOrTypeOrList) in a view model factory does not rebuild.

Is this expected?

marcglasberg commented 2 months ago

Yes, context.isWaiting(actionType) is supposed to rebuild, but simply using isWaiting(actionType) in the view model factory should NOT rebuild, just like accessing the state in the view-model should not rebuild.

If you want it to rebuild, the boolean must be part of the view model equals. See an example here:

https://github.com/marcglasberg/SameAppDifferentTech/blob/d82c8c9e1308e22b1978373834fca04a1bcd0463/MobileAppFlutterCelest/lib/client/portfolio_and_cash_screen/cash_balance/cash_balance_connector.dart#L45

class _Vm extends Vm {
  ...
  final bool isWaiting;

  _Vm({
    ...
    required this.isWaiting,
  }) : super(equals: [ ...
          isWaiting,
        ]);
}
kuhnroyal commented 2 months ago

Yea, I understand that. That is actually what I am doing but it does not work. The factory is not triggered when an action completes.

marcglasberg commented 2 months ago

I'll look into it!

marcglasberg commented 2 months ago

@kuhnroyal

I'm trying to reproduce this, but failing. Actually, I managed to reproduce in a specific situation, which is probably not the one you are having problems with. Could you please run this example (it's a single file) and tell me what you think?

https://raw.githubusercontent.com/marcglasberg/async_redux/master/example/lib/main_spinner_with_store_connector.dart

This example has 3 buttons:

I found it was not working only in the situation where the action failed and no dialog was opened. So I fixed this, and created a bunch of extra tests.

The fixed version is async_redux: ^23.0.2

Could you please test if this version solves the problem for you? I suspect it won't, unless you have the exact same situation I described above. In this case I need more information.

Anyway, please let me know.

kuhnroyal commented 2 months ago

Thanks! Will get back to you in 2 weeks. Currently in vacation.

kuhnroyal commented 1 month ago

I just got around to testing 23.0.2 but this didn't change anything. I will see if I can create a simple repro.

kuhnroyal commented 1 month ago

Managed to reproduce this. I think it is related to shouldUpdateModel.

The increment button will stay disabled here.

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

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  Widget build(BuildContext context) {
    return StoreProvider(
      store: Store<AppState>(initialState: AppState(counter: 0)),
      child: StoreConnector<AppState, CounterVm>(
        vm: () => CounterVmFactory(),
        shouldUpdateModel: (s) => s.counter >= 0,
        builder: (context, vm) {
          return Scaffold(
            appBar: AppBar(
              backgroundColor: Theme.of(context).colorScheme.inversePrimary,
              title: Text(title),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  const Text(
                    'You have pushed the button this many times:',
                  ),
                  Text(
                    '${vm.counter}',
                    style: Theme.of(context).textTheme.headlineMedium,
                  ),
                ],
              ),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: vm.isIncrementing ? null : vm.increment,
              backgroundColor: vm.isIncrementing ? Colors.grey : Theme.of(context).colorScheme.primary,
              tooltip: 'Increment',
              child: const Icon(Icons.add),
            ),
          );
        },
      ),
    );
  }
}

class AppState {
  final int counter;

  AppState({required this.counter});
}

class CounterVm extends Vm {
  final int counter;

  final bool isIncrementing;

  final VoidCallback increment;

  CounterVm(
    this.counter,
    this.isIncrementing,
    this.increment,
  ) : super(equals: [
          counter,
          isIncrementing,
        ]);
}

class CounterVmFactory extends VmFactory<AppState, MyHomePage, CounterVm> {
  @override
  CounterVm fromStore() => CounterVm(
        state.counter,
        isWaiting(IncrementAction),
        () => dispatch(IncrementAction()),
      );
}

class IncrementAction extends ReduxAction<AppState> {
  @override
  Future<AppState?> reduce() async {
    dispatch(DoIncrementAction());
    await Future.delayed(const Duration(seconds: 1));
    return null;
  }
}

class DoIncrementAction extends ReduxAction<AppState> {
  @override
  AppState reduce() {
    return AppState(counter: state.counter + 1);
  }
}