Code Monkey home page Code Monkey logo

async_redux's Introduction

pub package pub package

Async Redux | state management

  • Simple to learn and easy to use
  • Powerful enough to handle complex applications with millions of users
  • Testable

This means you'll be able to create apps much faster, and other people on your team will easily understand and modify your code.

What is it?

An optimized reimagined version of Redux. A mature solution, battle-tested in hundreds of real-world applications. Written from the ground up, created by Marcelo Glasberg (see all my packages).

There is also a version for React

Optionally use it with Provider or Flutter Hooks

Documentation

The complete docs are published at https://asyncredux.com

Below is a quick overview.


Store, state, actions and reducers

The store holds all the application state. A few examples:

// Here, the state is a number
var store = Store<int>(initialState: 1);
// Here, the state is an object
class AppState {
  final String name;
  final int age;
  State(this.name, this.age);
}

var store = Store<AppState>(initialState: AppState('Mary', 25));

 

To use the store, add it in a StoreProvider at the top of your widget tree.

Widget build(context) {
  return StoreProvider<int>(
    store: store,
    child: MaterialApp( ... ), 
    );                      
}

 

Widgets use the state

class MyWidget extends StatelessWidget {

  Widget build(context) {
    return Text('${context.state.name} has ${context.state.age} years old');
  }
}

 

Actions and reducers

An action is a class that contain its own reducer.

class Increment extends Action {

  // The reducer has access to the current state
  int reduce() => state + 1; // It returns a new state
}

 

Dispatch an action

The store state is immutable.

The only way to change the store state is by dispatching an action. The action reducer returns a new state, that replaces the old one.

// Dispatch an action
store.dispatch(Increment());

// Dispatch multiple actions
store.dispatchAll([Increment(), LoadText()]);

// Dispatch an action and wait for it to finish
await store.dispatchAndWait(Increment());

// Dispatch multiple actions and wait for them to finish
await store.dispatchAndWaitAll([Increment(), LoadText()]);

 

Widgets can dispatch actions

The context extensions to dispatch actions are dispatch , dispatchAll etc.

class MyWidget extends StatelessWidget {
 
  Widget build(context) { 
    return ElevatedButton(
      onPressed: () => context.dispatch(Increment());
    }     
}

 

Actions can do asynchronous work

They download information from the internet, or do any other async work.

var store = Store<String>(initialState: '');
class LoadText extends Action {

  // This reducer returns a Future  
  Future<String> reduce() async {
  
    // Download something from the internet
    var response = await http.get('https://dummyjson.com/todos/1');
    
    // Change the state with the downloaded information
    return response.body;      
  }
}

 

If you want to understand the above code in terms of traditional Redux patterns, all code until the last await in the reduce method is the equivalent of a middleware, and all code after that is the equivalent of a traditional reducer. It's still Redux, just written in a way that is easy and boilerplate-free. No need for Thunks or Sagas.

 

Actions can throw errors

If something bad happens, you can simply throw an error. In this case, the state will not change. Errors are caught globally and can be handled in a central place, later.

In special, if you throw a UserException, which is a type provided by Async Redux, a dialog (or other UI) will open automatically, showing the error message to the user.

class LoadText extends Action {
    
  Future<String> reduce() async {  
    var response = await http.get('https://dummyjson.com/todos/1');

    if (response.statusCode == 200) return response.body;
    else throw UserException('Failed to load');         
  }
}

 

To show a spinner while an asynchronous action is running, use isWaiting(action).

To show an error message inside the widget, use isFailed(action).

class MyWidget extends StatelessWidget {

  Widget build(context) {
    
    if (context.isWaiting(LoadText)) return CircularProgressIndicator();
    if (context.isFailed(LoadText)) return Text('Loading failed...');
    return Text(context.state);
  }
}

 

Actions can dispatch other actions

You can use dispatchAndWait to dispatch an action and wait for it to finish.

class LoadTextAndIncrement extends Action {

  Future<AppState> reduce() async {    
    
    // Dispatch and wait for the action to finish
    await dispatchAndWait(LoadText());
    
    // Only then, increment the state
    return state.copy(count: state.count + 1);
  }
}

 

You can also dispatch actions in parallel and wait for them to finish:

class BuyAndSell extends Action {

  Future<AppState> reduce() async {
  
    // Dispatch and wait for both actions to finish
    await dispatchAndWaitAll([
      BuyAction('IBM'), 
      SellAction('TSLA')
    ]);
    
    return state.copy(message: 'New cash balance is ${state.cash}');
  }
}

 

You can also use waitCondition to wait until the state changes in a certain way:

class SellStockForPrice extends Action {
  final String stock;
  final double limitPrice;
  SellStockForPrice(this.stock, this.limitPrice);

  Future<AppState?> reduce() async {  
  
    // Wait until the stock price is higher than the limit price
    await waitCondition(
      (state) => state.stocks[stock].price >= limitPrice
    );
      
    // Only then, post the sell order to the backend
    var amount = await postSellOrder(stock);    
    
    return state.copy(
      stocks: state.stocks.setAmount(stock, amount),
    ); 
}

 

Add features to your actions

You can add mixins to your actions, to accomplish common tasks.

Check for Internet connectivity

CheckInternet ensures actions only run with internet, otherwise an error dialog prompts users to check their connection:

class LoadText extends Action with CheckInternet {
      
   Future<String> reduce() async {
      var response = await http.get('https://dummyjson.com/todos/1');
      ...      
   }
}   

 

NoDialog can be added to CheckInternet so that no dialog is opened. Instead, you can display some information in your widgets:

class LoadText extends Action with CheckInternet, NoDialog { 
  ... 
  }

class MyWidget extends StatelessWidget {
  Widget build(context) {     
     if (context.isFailed(LoadText)) Text('No Internet connection');
  }
}   

 

AbortWhenNoInternet aborts the action silently (without showing any dialogs) if there is no internet connection.

 

NonReentrant

To prevent an action from being dispatched while it's already running, add the NonReentrant mixin to your action class.

class LoadText extends Action with NonReentrant {
   ...
   }

 

Retry

Add Retry to retry the action a few times with exponential backoff, if it fails. Add UnlimitedRetries to retry indefinitely:

class LoadText extends Action with Retry, UnlimitedRetries {
   ...
   }

 

UnlimitedRetryCheckInternet

Add UnlimitedRetryCheckInternet to check if there is internet when you run some action that needs it. If there is no internet, the action will abort silently and then retried unlimited times, until there is internet. It will also retry if there is internet but the action failed.

class LoadText extends Action with UnlimitedRetryCheckInternet {
   ...
   }

Debounce (soon)

To limit how often an action occurs in response to rapid inputs, you can add the Debounce mixin to your action class. For example, when a user types in a search bar, debouncing ensures that not every keystroke triggers a server request. Instead, it waits until the user pauses typing before acting.

class SearchText extends Action with Debounce {
  final String searchTerm;
  SearchText(this.searchTerm);
  
  final int debounce = 350; // Milliseconds

  Future<AppState> reduce() async {
      
    var response = await http.get(
      Uri.parse('https://example.com/?q=' + encoded(searchTerm))
    );
        
    return state.copy(searchResult: response.body);
  }
}

 

Throttle (soon)

To prevent an action from running too frequently, you can add the Throttle mixin to your action class. This means that once the action runs it's considered fresh, and it won't run again for a set period of time, even if you try to dispatch it. After this period ends, the action is considered stale and is ready to run again.

class LoadPrices extends Action with Throttle {  
  
  final int throttle = 5000; // Milliseconds

  Future<AppState> reduce() async {      
    var result = await loadJson('https://example.com/prices');              
    return state.copy(prices: result);
  }
}

 

OptimisticUpdate (soon)

To provide instant feedback on actions that save information to the server, this feature immediately applies state changes as if they were already successful, before confirming with the server. If the server update fails, the change is rolled back and, optionally, a notification can inform the user of the issue.

class SaveName extends Action with OptimisticUpdate { 
   
  async reduce() { ... } 
}

 

Events

Flutter widgets like TextField and ListView hold their own internal state. You can use Events to interact with them.

// Action that changes the text of a TextField
class ChangeText extends Action {
  final String newText;
  ChangeText(this.newText);    
 
  AppState reduce() => state.copy(changeText: Event(newText));
  }
}

// Action that scrolls a ListView to the top
class ScrollToTop extends Action {
  AppState reduce() => state.copy(scroll: Event(0));
  }
}

 

Persist the state

You can add a persistor to save the state to the local device disk.

var store = Store<AppState>(
  persistor: MyPersistor(),  
);  

 

Testing your app is easy

Just dispatch actions and wait for them to finish. Then, verify the new state or check if some error was thrown.

class AppState {  
  List<String> items;    
  int selectedItem;
}

test('Selecting an item', () async {   

    var store = Store<AppState>(
      initialState: AppState(        
        items: ['A', 'B', 'C']
        selectedItem: -1, // No item selected
      ));
    
    // Should select item 2                
    await store.dispatchAndWait(SelectItem(2));    
    expect(store.state.selectedItem, 'B');
    
    // Fail to select item 42
    var status = await store.dispatchAndWait(SelectItem(42));    
    expect(status.originalError, isA<>(UserException));
});

 

Advanced setup

If you are the Team Lead, you set up the app's infrastructure in a central place, and allow your developers to concentrate solely on the business logic.

You can add a stateObserver to collect app metrics, an errorObserver to log errors, an actionObserver to print information to the console during development, and a globalWrapError to catch all errors.

var store = Store<String>(    
  stateObserver: [MyStateObserver()],
  errorObserver: [MyErrorObserver()],
  actionObservers: [MyActionObserver()],
  globalWrapError: MyGlobalWrapError(),

 

For example, the following globalWrapError handles PlatformException errors thrown by Firebase. It converts them into UserException errors, which are built-in types that automatically show a message to the user in an error dialog:

Object? wrap(error, stackTrace, action) =>
  (error is PlatformException)
    ? UserException('Error connecting to Firebase')
    : error;
}  

 

Advanced action configuration

The Team Lead may create a base action class that all actions will extend, and add some common functionality to it. For example, getter shortcuts to important parts of the state, and selectors to help find information.

class AppState {  
  List<Item> items;    
  int selectedItem;
}

class Action extends ReduxAction<AppState> {

  // Getter shortcuts   
  List<Item> get items => state.items;
  Item get selectedItem => state.selectedItem;
  
  // Selectors 
  Item? findById(int id) => items.firstWhereOrNull((item) => item.id == id);
  Item? searchByText(String text) => items.firstWhereOrNull((item) => item.text.contains(text));
  int get selectedIndex => items.indexOf(selectedItem);     
}

 

Now, all actions can use them to access the state in their reducers:

class SelectItem extends Action {
  final int id;
  SelectItem(this.id);
    
  AppState reduce() {
    Item? item = findById(id);
    if (item == null) throw UserException('Item not found');
    return state.copy(selected: item);
  }    
}

To learn more, the complete Async Redux documentation is published at https://asyncredux.com


The AsyncRedux code is based upon packages redux by Brian Egan, and flutter_redux by Brian Egan and John Ryan. Also uses code from package equatable by Felix Angelov. The dependency injection idea in AsyncRedux was contributed by Craig McMahon. Special thanks: Eduardo Yamauchi and Hugo Passos helped me with the async code, checking the documentation, testing everything and making suggestions. This work started after Thomas Burkhart explained to me why he didn't like Redux. Reducers as methods of action classes were shown to me by Scott Stoll and Simon Lightfoot.

The Flutter packages I've authored:

My Medium Articles:

My article in the official Flutter documentation:


Marcelo Glasberg:
https://github.com/marcglasberg
https://linkedin.com/in/marcglasberg
https://twitter.com/glasbergmarcelo
https://stackoverflow.com/users/3411681/marcg
https://medium.com/@marcglasberg

async_redux's People

Contributors

a-abramov avatar aprzedecki avatar cgestes avatar danim1130 avatar darkcavalier11 avatar dave avatar gadfly361 avatar im-trisha avatar kuhnroyal avatar marcglasberg avatar parisholley avatar saibotma avatar sunlightbro avatar swch01 avatar tritao avatar yamauchieduardo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

async_redux's Issues

Connector test not working

I didn't find any tests related to Connectors in examples or in the repository, so I started implementing myself.

I expect that after tapping the Login button, LoginUserAction is dispatched. The console prints out the intialization(INI) and end(END) of the LoginUserAction, but the test is running for minutes and times out. My repository is mocked out. What I do wrong?

LoginPageConnector

class LoginPageConnector extends StatelessWidget {
  LoginPageConnector({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, ViewModel>(
        model: ViewModel(),
        builder: (BuildContext context, ViewModel vm) => LoginPage(
              loading: vm.loading,
              message: vm.message,
              onCredentialsProvided: vm.onCredentialsProvided,
            ));
  }
}

class ViewModel extends BaseModel<AppState> {
  ViewModel();

  bool loading;
  String message;
  void Function(String, String) onCredentialsProvided;

  ViewModel.build({@required this.loading,
    this.message,
    @required this.onCredentialsProvided})
      : super(equals: [loading]);

  @override
  BaseModel fromStore() =>
      ViewModel.build(
          loading: state.loading,
          message: state.message,
          onCredentialsProvided: (String email, String password) =>
              dispatchFuture(LoginUserAction(email: email, password: password)));
}

UserRepository

class UserRepository {
  http.Client client = httpClient;

  Future<LoginResponse> login({String email, String password}) async {
    var response = await callLoginAPI(email, password);
    final statusCode = response.statusCode;
    if (statusCode == 200) {
      return LoginResponse(token: response.headers['authorization']);
    } else if (statusCode == 400) {
      return LoginResponse(message: "Payload error, login can't be processed!");
    } else if (statusCode == 401) {
      return LoginResponse(message: "Email or password incorrect!");
    } else if (statusCode == 500) {
      return LoginResponse(
          message: "Server unresponsive, please try again later!");
    } else {
      return LoginResponse(
          message: "Application error, login can't be processed!");
    }
  }

  Future<http.Response> callLoginAPI(String email, String password) async =>
      await client.post('http://192.168.0.137:7071/api/login',
          body: JsonMapper.serialize(LoginRequest(LoginDto(email, password))));
}

LoginUserAction

class LoginUserAction extends ReduxAction<AppState> {
  final String email;
  final String password;
  final UserRepository userRepository = UserRepository();
  final secureStorage = SecureStorageProvider();

  LoginUserAction({this.email, this.password})
      : assert(email != null && password != null);

  @override
  Future<AppState> reduce() async {
    try {
      final loginResponse =
          await userRepository.login(email: email, password: password);
      final token = loginResponse.token;
      if (token != null) {
        secureStorage.persistToken(token);
        addClaimsToStore(token);
        dispatch(NavigateAction.pushNamed('/start'));
      } else {
        return state.copy(message: loginResponse.message);
      }
    } catch (e) {
      print(e);
    }
    return null;
  }

  void addClaimsToStore(String token) async {
    var rawClaims = Jwt.parseJwt(token);
    JsonMapper().useAdapter(JsonMapperAdapter(valueDecorators: {
      typeOf<List<UserWallet>>(): (value) => value.cast<UserWallet>(),
    }));
    final claims = JsonMapper.deserialize<ClaimState>(rawClaims);
    dispatchFuture(AddClaimAction(claims: claims));
  }
}

login_page_CONNECTOR_test

class MockClient extends Mock implements http.Client {}

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  initializeReflectable();
  StoreTester<AppState> createStoreTester() {
    var store = Store<AppState>(initialState: AppState.initialState());
    return StoreTester.from(store);
  }

  MockUserRepository userRepository;
  setUp(() {
    userRepository = MockUserRepository();
  });

  testWidgets('LoginUserAction test', (WidgetTester tester) async {
    await tester.runAsync(() async {
      when(userRepository.login(
              email: anyNamed('email'), password: anyNamed('password')))
          .thenAnswer((_) async => LoginResponse(token: 'token'));
      var storeTester = createStoreTester();

      await tester.pumpWidget(
        StoreProvider<AppState>(
            store: createStoreTester().store,
            child: MaterialApp(home: LoginPageConnector())),
      );

      var emailInputFinder = find.byKey(Key('email-input'));
      await tester.enterText(emailInputFinder, '[email protected]');

      var passwordInputFinder = find.byKey(Key('password-input'));
      await tester.enterText(passwordInputFinder, '123abc');

      var loginButtonFinder = find.byKey(Key('login-button'));
      await tester.tap(loginButtonFinder);
      await tester.pump();

      var waitUntil = await storeTester.waitUntil(LoginUserAction);

      expect(waitUntil.action, LoginUserAction);
    });
  });
}

Console output

New StoreTester.
New StoreTester.
D:1 R:0 = Action LoginUserAction INI
D:1 R:1 = Action LoginUserAction END
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following StoreExceptionTimeout was thrown while running async test code:
Timeout.

When the exception was thrown, this was the stack:
#0      StoreTester._next.<anonymous closure> (package:async_redux/src/store_tester.dart:577:30)
#13     _Timer._runTimers (dart:isolate-patch/timer_impl.dart:384:19)
#14     _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:418:5)
#15     _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:174:12)
(elided 12 frames from package dart:async, package dart:async-patch, and package stack_trace)
════════════════════════════════════════════════════════════════════════════════════════════════════

Test failed. See exception logs above.
The test description was: LoginUserAction test

Inconsistency in observers while dispatching an async action

According to the documentation on observers:

... getting an END action observation doesn't mean ... However, it does mean that the action can no longer change the state directly.

This does not seem to hold when dispatching an async Action using the normal dispatch function:

import 'dart:async';

import 'package:async_redux/async_redux.dart';
import "package:test/test.dart";

class _MyAction extends ReduxAction<num> {
  final num number;

  _MyAction(this.number);

  @override
  FutureOr<num> reduce() => number;
}

class _MyAsyncAction extends ReduxAction<num> {
  final num number;

  _MyAsyncAction(this.number);

  @override
  FutureOr<num> reduce() async{
    await Future.sync((){});
    return number;
  }
}

class _MyStateObserver extends StateObserver<num>{
  num iniValue;
  num endValue;

  @override
  void observe(ReduxAction<num> action, num stateIni, num stateEnd, int dispatchCount) {
    iniValue = stateIni;
    endValue = stateEnd;
  }

}

void main() {

  var observer = _MyStateObserver();
  StoreTester<num> createStoreTester() {
    var store = Store<num>(
      initialState: 0,
      stateObservers: [observer]
    );
    return StoreTester.from(store);
  }

  ///////////////////////////////////////////////////////////////////////////////

  test(
      'Dispatch a sync action, see what the StateObserver picks up. ', () async {
    var storeTester = createStoreTester();
    expect(storeTester.state, 0);

    storeTester.dispatch(_MyAction(1));
    var condition = (TestInfo<num> info) => info.state == 1;
    TestInfo<num> info1 = await storeTester.waitConditionGetLast(condition);
    expect(observer.iniValue, 0);
    expect(observer.endValue, 1);

  });

  ///////////////////////////////////////////////////////////////////////////////

  test(
      'Dispatch an async action, see what the StateObserver picks up.', () async {
    var storeTester = createStoreTester();
    expect(storeTester.state, 0);

    storeTester.dispatch(_MyAsyncAction(1));
    var condition = (TestInfo<num> info) => info.state == 1;
    TestInfo<num> info2 = await storeTester.waitConditionGetLast(condition);
    expect(observer.iniValue, 0);
    expect(observer.endValue, 1);

  });

  ///////////////////////////////////////////////////////////////////////////////

}

This seems to happen because while the _processAction function is async, the normal dispatch function doesn't wait for it to finish, and so the execution continues to the observers even when the action itself hasn't completed. This seems to defeat the purpose of observers (and the persistor).

Is this the expected behavior (and an error in the documentation), or a bug?

Provide official useSelector and useDispatch hooks (for flutter_hooks)

I'm a Flutter noob trying to get the nice state management stack that I'm accustomed to from react-redux. The combination of useSelector and useDispatch hooks is the cleanest and most concise state management strategy I've yet encountered, especially combined with reselect.

The flutter_hooks package seems to provide similar react hooks functionality, but it appears that the dirty-checking model is totally different. For example useState returns a ValueNotifier wrapped value instead of just the actual value. I'm afraid it's currently beyond my Flutter experience to understand why that is and how to use it to get what I want.

So far I have this:

// seems to work ok, probably not done correctly
useDispatch() {
  final BuildContext context = useContext();

  final redux.Store<AppState> store =
      redux.StoreProvider.of<AppState>(context, false);

  return store.dispatch;
}

And this:

// Does NOT re-render the widget when store state changes
useSelector(Function selectorFn) {
  final BuildContext context = useContext();
  final AppState state = redux.StoreProvider.state<AppState>(context);

  return selectorFn(state);
}

It seems that @marcglasberg and @rrousselGit have collaborated successfully in the past. Maybe you guys can help me see this dream accomplished? Let me know if I should move this ticket to a different repo.

WaitAction with BuiltValue

Hello,

I like very much the WaitAction because when you have a lot a loading it can pollute you state classes.
But I use BuiltValue for immutability of the State and i think it's a popular choice among dart devs.
Could you add a way to make it work with BuiltValue ?

Maybe something like this

try {
    return (state as dynamic).rebuild((state) => 
        state..wait = state.wait.process(operation, flag: flag, ref: ref));
} catch (_) {
      return (state as dynamic).copy(wait: wait.process(operation, flag: flag, ref: ref));
}

Add StackTrace to wrapError and ErrorObserver

Dart API and syntax is a bit wired when it comes to exceptions. Exception is an object that doesn't include a stack trace information. The stack trace is provided as a second optional parameter in catch capsule eg:

try {
  throw UserError();
} catch(e, stackTrace) {
  action.wrapError(e, stackTrace);
}

Having StackTrace object in wrapError and ErrorObserver interface would enable us to send both to error reporting services like Crashlytics and also make easier to understand the underlying issue.

How to extract part of Actions into separate module (package)

Hi,

We are working on two very similar apps sharing a lot of logic and a common backend.

We would like to extract our login screens and corresponding logic (actions) into a package and import it into both apps.

The problem is how to get the app's AppState inside the package logic:

class LoginAction extends ReduxAction<AppState>

The only solution we came up with is to have a separate store inside the package something like LoginState and then use it like this

module:

class LoginAction extends ReduxAction<LoginState>

app:

class AppState {
    LoginState get loginState => loginStore.state;
}

Thank you.

Navigate actions do not support pushing and popping to get value from route

First off I am skeptical that having redux know about the idea of Flutter navigation is a good idea, though I have created my own abstraction for it before but it feels dirty.

However if it is going to exist it should probably support the notion of pushing routes returning values via passing a value to pop. Not sure how that could be implemented. You have dispatchFuture which returns a Future perhaps this can be extends to have async actions returning values and dispatchFuture return a Future?

Display Dialog or SnackBar from Action's before method

Hi,

I really enjoy using your library. It makes it so much easier to use the Redux with Flutter. Thank you.

I would like to ask how can I get a correct context to be able to display Dialog or SnackBar from the before method inside an abstract class.

abstract class BarrierAction extends ReduxAction<AppState> {
  Future before() async {
    dispatch(SetLoadingAction(true));
    if (!hasConnection) {
      showDialog(
          context: --> ???,
          builder: /* check your internet connection modal */
       )
  }
}

I was hoping something like navigatorKey.currentState.context would work but I wasn't successful.

I have also tried passing current context to the Action itself -> store.dispatch(SomeAction(context)) but I don't know how to pass it into the before method without overriding it, which adds (maybe) unnecessary complexity.

Thank you for your advice.

Dispatch Action from reduce fonction

Hello,

I'm trying AsyncRedux and i found it really great.
Nevertheless I have a question about the Action.

If I dispatch an action2 from the reduce fonction of action1, the action2 is executed before the store gets updated from the action1. If the action2 needs that updated data, it's a problem.

So I tried to dispatch the action2 in the after method of the action1.
It works but if a have an error in the action1, I don't want the action2 dispatched but the after method is called anyway.

Do you have a solution ?
Thanks you.

Roadmap Question

Hi, I'm new to Redux but this package looks very nice, so thank you! I have a question about plans for enhancements. One thing this package doesn't seem to support is redux_dev_tools and flutter_redux_dev_tools, since they require replacing the store with a dev store for "time travel" for purposes of development and testing. As I said, I'm new to Redux but that dev feature sounds very compelling and Redux architecture in general seems like a great fit for it. Do you have any plans to support "time travel" in this library? No worries if not, but if so, I think it would be yet another benefit of using this package. Having said that, I could be wrong. Maybe "time travel" isn't used very often at all by Redux devs? Thanks in advance for your thoughts on this, and thanks for the awesome library.

Recommendation on how to dispatch events based on state changes

Hi,

I would like to react to my authentication state and be able to emit events when it changes.

For now I do that outside of the store, I wonder if there is a simple way to subscribe to state changes and react to them.

I envision something that looks like this:

void setupAuthStateChanges(store) {
  // we have a selector for our changes
  var selector = (state) => state.auth.profile; //assuming this is immutable

  // we have an action in response
  var action = (store, profile) {
    if (profile == null)
      store.dispatch(DeInitSomethingElse());
    else
      store.dispatch(InitSomethingElse());
  }

  // we register the action and the selector
  subscription = store.onChange(selector, action);

  // later on we can cancel the subscription
  subscription.cancel();
}

Seems pretty similar to StoreConnector but without the widget logic.

I wonder how you do it guys.

Action and state observers run in an unintuitive order w.r.t. each other

When trying to use the action observers and state observers at the same time, I noticed that the action and state observers run in this order:

  • action observer, ini = true
  • action observer, ini = false
  • state observer

However, this was not quite intuitive to me. I was expecting the order to be:

  • action observer, ini = true
  • state observer
  • action observer, ini = false

Is my expectation correct? If not, feel free to close this issue 👍


I discovered this when trying to dispatch an action, that in turn dispatched an action in its before and after methods.

Here is a diagram of what I was doing.

async_redux_observer_order_miro_board2

This is the order of action and state observers as of v1.3.3:

async_redux_observer_order

Cancelable Actions

Is there a recommended way to make an async Action cancelable?

This is a "Best Practice" question since I am very unexperienced with redux.

I am doing http requests and the user may cancel them. For this my http client takes an additional parameter "CancelToken".

I create a CancelToken before I dispatch the action that starts the http request. Then I can cancel the http request by calling cancelToken.cancel().

Now, I have to somehow make the cancel token available to the Cancel button, and I guess the only way is through the store, correct? But a cancel token is not a simple data object, but a complex object. Is it still ok to put it in the store?

Is there a recommended way of making Actions cancelable?

Thanks!

my state is not changin

I have a problem I can't find the bug my state is not changing

the main file

import 'package:Saree3/Graphql/client_provider.dart';
import 'package:Saree3/store/AppState.dart';
import 'package:async_redux/async_redux.dart';
import 'package:flutter/material.dart';
import 'package:Saree3/Constants/Constants.dart';
import 'package:Saree3/Screens/SplashScreen.dart';
import 'package:Saree3/Screens/HomeScreen.dart';
import 'package:Saree3/Screens/OtpScreen.dart';

Store<AppState> store;
void main() {
  var state = AppState.initialState();
  store = Store<AppState>(initialState: state);

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreProvider<AppState>(
      store: store,
      child: MaterialApp(
        // home: new Splash(),
        initialRoute: OTP_SCREEN,
        debugShowCheckedModeBanner: false,
        theme: new ThemeData(
          primaryColorDark: Colors.brown,
          primaryColor: Colors.blueAccent,
        ),
        routes: {
          HOME_SCREEN: (BuildContext context) => new HomeScreen(),
          OTP_SCREEN: (BuildContext context) => new OtpScreenConnector(),
        },
        builder: (context, child) {
          return Directionality(textDirection: TextDirection.rtl, child: child);
        },
      ),
    );
  }
}

OtpScreen

import 'package:Saree3/actions/User.dart';
import 'package:Saree3/store/AppState.dart';
import 'package:async_redux/async_redux.dart';
import 'package:flutter/material.dart';
import 'package:Saree3/Constants/Constants.dart';
import 'package:flutter/services.dart';

class OtpScreen extends StatefulWidget {
  final String firstName;
  final ValueChanged<String> signUp;

  OtpScreen({Key key, this.firstName, this.signUp}) : super(key: key);

  @override
  OtpScreenState createState() => OtpScreenState();
}

class OtpScreenState extends State<OtpScreen> {
  final _formKey = GlobalKey<FormState>();

  TextEditingController firstNameController;

  @override
  void initState() {
    super.initState();
    firstNameController = TextEditingController();
  }

  @override
  void dispose() {
    firstNameController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Form(
      key: _formKey,
      child: Container(
          padding: EdgeInsets.all(30),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              TextFormField(
                controller: firstNameController,
                onChanged: (text) {
                  // print(text);
                },
                autofocus: true,
                validator: (value) {
                  if (value.isEmpty) {
                    return 'لو سمحت املأ الفراغات';
                  }
                  return null;
                },
                decoration: InputDecoration(
                    border: OutlineInputBorder(),
                    labelText: "الإسم الأول",
                    hintText: 'أدخل إسمك الأول'),
              ),
              SizedBox(
                height: 20,
              ),
              TextFormField(
                onChanged: (text) {
                  // print(text);
                },
                autofocus: true,
                validator: (value) {
                  if (value.isEmpty) {
                    return 'لو سمحت املأ الفراغات';
                  }
                  return null;
                },
                decoration: InputDecoration(
                    border: OutlineInputBorder(),
                    labelText: "الإسم الثاني",
                    hintText: 'أدخل إسمك الثاني'),
              ),
              SizedBox(
                height: 20,
              ),
              TextFormField(
                keyboardType: TextInputType.number,
                inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
                onChanged: (text) {
                  // print(text);
                },
                autofocus: true,
                validator: (value) {
                  if (value.isEmpty) {
                    return 'لو سمحت املأ الفراغات';
                  }
                  return null;
                },
                decoration: InputDecoration(
                    border: OutlineInputBorder(),
                    labelText: "رقم الجوال",
                    hintText: 'أدخل رقم الجوال'),
              ),
              Padding(
                  padding: const EdgeInsets.symmetric(vertical: 16),
                  child: RaisedButton(
                    onPressed: () {
                      widget.signUp(firstNameController.text);
                    },
                    child: Text("تسجيل"),
                  )),
              Text('${widget.firstName}')
            ],
          )),
    ));
  }
}

class OtpScreenConnector extends StatelessWidget {
  OtpScreenConnector({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, ViewModel>(
      model: ViewModel(),
      builder: (BuildContext context, ViewModel vm) => OtpScreen(
        firstName: vm.firstName,
        signUp: vm.signUp,
      ),
    );
  }
}

class ViewModel extends BaseModel<AppState> {
  ViewModel();
  String firstName;
  ValueChanged<String> signUp;
  ViewModel.build({@required this.firstName, @required this.signUp});

  @override
  ViewModel fromStore() => ViewModel.build(
      firstName: state.userState.firstName,
      signUp: (String firstName) => dispatch(SignUp(firstName)));
}

the app state -- main state

import 'package:Saree3/store/UserState.dart';

class AppState {
  final UserState userState;

  AppState({this.userState});

  AppState copy({
    UserState userState,
  }) {
    return AppState(
      userState: userState ?? this.userState,
    );
  }

  static AppState initialState() =>
      AppState(userState: UserState.initialState());

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is AppState &&
          runtimeType == other.runtimeType &&
          userState == other.userState;

  @override
  int get hashCode => userState.hashCode;
}

var state = AppState.initialState();

user State

class UserState {
  final String firstName;
  UserState({this.firstName});

  UserState copy({String firstName}) =>
      UserState(firstName: firstName ?? this.firstName);

  static UserState initialState() => UserState(firstName: "mohammad");
  @override
  bool operator ==(other) =>
      identical(this, other) ||
      other is UserState &&
          runtimeType == other.runtimeType &&
          firstName == other.firstName;

  @override
  int get hashCode => firstName.hashCode;
}

tha signup action and reduce

import 'package:Saree3/store/AppState.dart';
import 'package:async_redux/async_redux.dart';

class SignUp extends ReduxAction<AppState> {
  final String firstName;

  SignUp(this.firstName);

  @override
  AppState reduce() =>
      state.copy(userState: state.userState.copy(firstName: firstName));
}
``



Making StoreConnector independent of BaseModel?

I began migrating my app to async_redux and so far, everything went fine.
However, I find one thing a bit annoying:
I already have ViewModels, and I used built_model to create them.
I cannot use these directly, as StoreConnector has a type Constraint to BaseModel.
class StoreConnector<St, Model extends BaseModel>
It seems that the only real reason for this is line 921 in store.dart:
return widget.model.fromStore();

This line would not compile without the type Constraint to BaseModel.
However, this line performs an unsafe typecast anyway, as dart implicitly downcasts the BaseModel to the generic Model type, as showcased by this code snippet:

class ViewModelA extends BaseModel{
  @override
  BaseModel fromStore() {
    return ViewModelA();
  }

}

class ViewModelB extends BaseModel{
  @override
  BaseModel fromStore() {
    return ViewModelB();
  }
}

class MyWidget extends StatelessWidget {

  @override
    Widget build(BuildContext context) {
      return  StoreConnector<AppState, ViewModelA>(
        model: ViewModelB(),
        builder: (_,__) => Container());
}
}

This fails at runtime with the error message "type ViewModelB is not a subtype of ViewModelA".
This is unlikely to be a real issue, as it would get caught by the developer pretty easily, but I propose to make this cast explicit:
return widget.model.fromStore() as Model;.

This would also make it possible to remove the type constraint and I could use my already existing BuiltValue models.
I will open a pull request with this change shortly.

If you don't like this change, I can think of another solution:

  • Implementing a CustomStoreConnector, that would pretty much the same as StoreConnector but doesn't use BaseModel. Trivial, but unnecessary code duplication imo.

What's the recommend place to manage business logic from

Current implementation introduces two places that can hold business logic

ViewModel functions, like onIncrement in this example

class ViewModel extends BaseModel<AppState> {
  ViewModel();

  int counter;
  String description;
  VoidCallback onIncrement;

  ViewModel.build({
	@required this.counter,
	@required this.description,
	@required this.onIncrement,
  }) : super(equals: [counter, description]);

  @override
  ViewModel fromStore() => ViewModel.build(
		counter: state.counter,
		description: state.description,
		onIncrement: () => dispatch(IncrementAndGetDescriptionAction()),
	  );
}

And Action functions, like reduce in this example

class IncrementAndGetDescriptionAction extends ReduxAction<AppState> {

  @override
  Future<AppState> reduce() async {
	dispatch(IncrementAction());
	String description = await read("http://numbersapi.com/${state.counter}");
	return state.copy(description: description);
  }

  void before() => dispatch(WaitAction(true));

  void after() => dispatch(WaitAction(false));
}

In async_redux What is the recommend place to manage business logic from?
And by business logic, I mean something like

if(isDataExpired){
   // get new data from some web api
}
else if(IsUserNotLoggedIn){
  // route user to login screen
}else{
 // do whatever we need to do
}

Bug: StoreConnector shouldUpdateModel

In the StoreConnector, the documentation of the shouldUpdateModel model says :

To ignore a change, provide a function that returns true or false. If the returned value is false, the change will be ignored

But the view is rebuilt when the returned value is false and ignored when the value is true.

I guess the problem is here in the _StoreStreamListener

if (widget.shouldUpdateModel != null) {
_stream = _stream.where((state) => !widget.shouldUpdateModel(state));
}

ignoreChange is named confusingly

I think, the ignoreChange parameter of StoreConnector is named confusingly.
Imagine this example:

Widget build(BuildContext context) {
    return StoreConnector<AppState, Model>(
      model: Model(),
      ignoreChange: (state) => true,
      builder: (context, model) => Container(),
    );
  }

My intuitive understanding of this code would be, that since ignoreChange() evaluates to true, all changes to the state are ignored and the child is never rebuilt due to a change in the app state.
However, as the documentation correctly states:

To ignore a change, provide a function that returns true or false. If the returned value is false, the change will be ignored., meaning that ignoreChange needs to return false in order to ignore changes.

It might be worth it, now that this library is still pretty new, to rename this parameter to something like "shouldUpdateModel" and to deprecate ignoreChange.

Minor Editing Inconsistencies in the README.md

While reading the README.md, I've noted some inconsistencies that might make the document less readable or harder to maintain, albeit only slightly — the numbering is only for you to more easily refer to each item —:

  1. Sometimes there are hard tabs (\t) and sometimes only spaces as tabs. Which one is the norm for this repo?
    • Many editors convert tabs to spaces under the hood, which could be a solution to the tabs vs spaces dilemma.
  2. Usually it is recommended to use []() for links instead of <a> tags in Markdown.
  3. In some (un)ordered lists, the content of subsections or code-blocks does not follow the indentation of the list.
  4. Some code-blocks have not been properly indented.
  5. Sometimes - are used for unordered lists, sometimes it's *. Which one is the norm for this repo?
    • Most people recommend -.
  6. Some ordered lists are not properly numbered. This could be avoided if there were no numbering at all, only prefixing the items with 1..
  7. You opted for breaking paragraphs and phrases internally with single line breaks. I don't know what's behind this decision, but it seems unnecessary to me since editors do the line breaks in the background for readability anyway. At any rate, the main problem with this, to me, is that I can't really figure out a rule for when I or another contributor should do an internal line break.

Which of the above would you like fixed? All? None and they are all irrelevant?

I would also like to mention that it seems to be very difficult to maintain such a huge README.md. I would suggest breaking it into more concise files and creating a minimum minimorum guide for those starting out.

If you break each section into a single file, there are mini programs/extensions to put them all together sequentially inside the README.md again.

How disable print in test async_redux?

Hello,

I wanna to do all tests in my project, but I have a lot of prints of the async_redux lib.

I can disable this prints? This is very helpfull, but I wanna know if it this can be disable too.

Thanks for the package.

Unable to test PersistAction

All StoreTester.wait* methods fail when using PersistAction as the Type can not be matched.
I have not found a way to specify the correct type.

Maybe the matching can be enhanced to ignore the generic parameter of the PersistAction or some alternative wait* methods can be added that expect Matcher instances, like [isA<PersistAction<MyState>>(), equals(fooAction)].

Got this action: PersistAction<MyState> INI.
Was expecting: PersistAction<dynamic> INI.

StoreConnector not being rebuilt if ViewModel includes list of enums

Hey there,
I may have found a bug, or maybe I've some errors in my implementation. I've created following connector:

class MiniFilterDisplayConnector extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, ViewModel>(
      model: ViewModel(),
      distinct: true,
      builder: (context, model) => MiniFilterDisplay(
        activeCriteria: model.appliedCriteria,
        onCriteriaRemoved: model.onCriteriaRemoved,
      ),
    );
  }
}

class ViewModel extends BaseModel<AppState> {
  ViewModel();

  List<CriteriaKind> appliedCriteria;
  Function(CriteriaKind) onCriteriaRemoved;

  ViewModel.build({
    @required this.appliedCriteria,
    @required this.onCriteriaRemoved,
  }) : super(equals: [appliedCriteria]);

  @override
  BaseModel fromStore() => ViewModel.build(
        appliedCriteria: state.appliedCriteria,
        onCriteriaRemoved: (criteria) {
          dispatch(RemoveAndCopyCriteriaAction(criteriaKindToRemove: criteria));
          dispatch(ApplyFilterAction());
        },
      );
}

It should be rebuilt each time the contents of the list appliedCriteria change. However, the child widget isn't being rebuilt as that list changes. I'm pretty sure about that because:

  • I'm logging the app's state and state.appliedCriteria is changing as expected
  • After firing the action that changes state.appliedCriteria, if I do a hot-reload, the child widget is built using the new values of the list

I think the problem is related somehow to the fact that the list is a list of an enum type, and those lists, in Dart, have to be compared using listEquals() function.

I've tried to implement my own == ViewModel's operator instead of using the super(equals: [appliedCriteria]) thing, like this:

  @override
  int get hashCode => this.appliedCriteria.hashCode;

  @override
  bool operator ==(Object other) {
    return (identical(this, other) ||
        other is ViewModel &&
            listEquals(this.appliedCriteria, other.appliedCriteria));
  }

but it didn't work neither.
I've used that ViewModel approach many times in my app with other types in the super(equals: [...]) function and it always worked fine, so I think it should be defineltly related to the fact of being an enum list.
Any comment or idea will be helpful!

SubState clarification

Hi,

thanks for async_redux it rocks!
I'am currently playing with it and I have some questions.

In https://pub.dev/packages/async_redux#state-declaration you propose to split the state in multiple classes. (like we would do in Redux).

I wonder what to do with actions, should they still inherit from ReduxAction and have some kind of nested copy for the reducer like in the following example:

class AddTodoAction extends ReduxAction<AppState> {
  final Todo todo;

  AddTodoAction(this.todo);

  @override
  AppState reduce() {
    return state.copy(todos: state.todos.copy(todos: todos.add(todo)));
  }
}

Is there a way to avoid cascading the copies?
Also is the copy of the AppState needed when a substate changes, looks like the operator== and the hashCode would detect the change anyway.

Thank you :)

NavigateActions - is it possible to set transition type?

Hi,

Firstly – we really love this library and our whole team is using it in every Flutter app we develop. Thank you for your hard work.

I would like to ask if it's possible to change route transition when using NavigateActions.

So far I have created custom Action with this code (using page_transition library) which works right now:

navigatorKey.currentState.push(PageTransition(type: PageTransitionType.fade, child: route));

My goal is to change all routes in my app to have custom transition which I haven't been able to achieve yet in some global setting (no clear documentation on how to do that).

Thank you.

StoreConnector for each view of a GridView is refreshing wrong data model when items from GridView are updated

Hi,

I'm new in Flutter and I really like this library, very intuitive and easy to use indeed.
However I'm facing an issue when I'm using a StoreConnector for each view of a GridView which is himself affected by a StoreConnector when data list changed.

I have in my flutter app a tab for favorites and his screen shows a gridView of items (Sheets)

favorites_view_model.dart

import 'package:async_redux/async_redux.dart';
import 'package:business/app_state.dart';
import 'package:business/sheets/models/sheet.dart';
import 'package:business/view_state/models/view_state.dart';
import 'package:flutter/foundation.dart';

class FavoritesViewModel extends BaseModel<AppState>{

  FavoritesViewModel();

  ViewState<List<Sheet>> viewStateSheets;
  Set<int> favorites;

  FavoritesViewModel.build({
    @required this.viewStateSheets,
    @required this.favorites,
  });

  @override
  FavoritesViewModel fromStore() => FavoritesViewModel.build(viewStateSheets: state.viewStateSheets, favorites: state.favorites);

  @override
  bool operator == (Object other) {
    return identical(this, other) || other is FavoritesViewModel && runtimeType == other.runtimeType &&
    viewStateSheets == other.viewStateSheets && favorites == other.favorites && setEquals(favorites, other.favorites);
  }

  @override
  int get hashCode => viewStateSheets.hashCode ^ favorites.hashCode;
}

For each view inside my GridView, I'm using this SheetViewModel :

sheet_view_model.dart

import 'package:async_redux/async_redux.dart';
import 'package:business/app_state.dart';
import 'package:business/sheets/models/sheet.dart';
import 'package:business/view_state/models/view_state.dart';
import 'package:flutter/foundation.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class SheetViewModel extends BaseModel<AppState>{

  Sheet sheet;
  bool isLiked;
  LatLng userLocation;

  SheetViewModel({this.sheet});

  SheetViewModel.build({
    @required this.sheet,
    @required this.isLiked,
    @required this.userLocation,
  });

  @override
  SheetViewModel fromStore() => SheetViewModel.build(sheet: sheet, isLiked: state.favorites.contains(sheet.id), userLocation: state.userLocation);

  @override
  bool operator == (Object other) => identical(this, other) || other is SheetViewModel && runtimeType == other.runtimeType &&
      sheet == other.sheet && isLiked == other.isLiked && userLocation == other.userLocation;

  @override
  int get hashCode => sheet.hashCode ^ isLiked.hashCode ^ userLocation.hashCode;
}

The widgets (I'm using this file in favorites screen and list screen) :

gridview_sheets_widget.dart

import 'dart:math';
import 'package:async_redux/async_redux.dart';
import 'package:business/app_state.dart';
import 'package:client/sheet_view_model.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:business/sheets/models/sheet.dart';
import 'package:client/experimentations/triangle_percentage.dart';
import 'package:client/roots/root_screen_widget.dart';
import 'package:business/favorites/actions/add_favorite_action.dart';
import 'package:business/favorites/actions/remove_favorite_action.dart';


import '../app.dart';

const double commonPadding = 8;

GridView gridViewSheetsWidget(BuildContext context, List<Sheet> items, OnSheetClick onSheetClick,
    {double paddingTop = commonPadding * 2, double paddingBottom = commonPadding * 2}) {
  return GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 2 / 3 - 1 / 11),
      itemCount: items.length,
      padding: EdgeInsets.only(top: paddingTop, bottom: paddingBottom /*vertical: 0commonPadding * 2*/),
      itemBuilder: (BuildContext context, int position) {
        return _getSheetViewStoreConnector(context, items, position, onSheetClick);
      });
}

Widget _getSheetViewStoreConnector(BuildContext context, List<Sheet> items, int position, OnSheetClick onSheetClick) {
  return StoreConnector<AppState, SheetViewModel>(
      model: SheetViewModel(sheet: items[position]),
      distinct: true,
      builder: (BuildContext context, SheetViewModel vm) => _getSheetViewHolderWidget(context, position, vm.sheet, vm.isLiked, vm.userLocation, onSheetClick)
  );
}

Widget _getSheetViewHolderWidget(BuildContext context, int position, Sheet sheet, bool isLiked, LatLng userLocation, OnSheetClick onSheetClick){
...
}
=> add one sheet to favorite
I/flutter ( 5338): Model D:22 R:22 = Rebuid:true, , Model:FavoritesViewModel{}.
I/flutter ( 5338): Model D:22 R:22 = Rebuid:true, , Model:SheetViewModel{}.
I/flutter ( 5338): Model D:22 R:22 = Rebuid:true, , Model:SheetViewModel{}.
I/flutter ( 5338): favorites: (59: Fragonard Parfumeur, 2299: AVEL CHAR A VOILE, 2300: LE ROC DES HARMONIES)
=> remove one sheet from favorites view
I/flutter ( 5338): Model D:23 R:23 = Rebuid:true, , Model:FavoritesViewModel{}.
I/flutter ( 5338): Model D:23 R:23 = Rebuid:true, , Model:SheetViewModel{}.
I/flutter ( 5338): Model D:23 R:23 = Rebuid:false, , Model:SheetViewModel{}.
I/flutter ( 5338): Model D:23 R:23 = Rebuid:false, , Model:SheetViewModel{}.
I/flutter ( 5338): favorites: (2299: AVEL CHAR A VOILE, 2300: LE ROC DES HARMONIES)

Some sheets (items) are not showing the correct sheet view when updating the GridView. It's like StoreConnector of each view was cached and not refreshed with the new sheet (item). I'm using GridView Builder.

Any help would be appreciated.

bug: BaseModel.equals is not working for List

BaseModel.equals uses listEquals to compare the properties from two models, but it's kind of broken if one of the properties is a List:

import 'package:async_redux/async_redux.dart';

class ViewModel extends BaseModel<List<int>> {
  List<int> list;

  ViewModel(this.list) : super(equals: [list]);

  @override
  BaseModel fromStore() {
    return ViewModel(state);
  }
}

main() {
  // [1]
  final vm1 = ViewModel([1]);
  // [2]
  final vm2 = (vm1..list[0] = 2);

  // assertion failed
  assert(vm1 != vm2);
}

Debounce (protect against accidental multiple operations)

How to implement protection against too often heavy operations?
For instance, if I pressed the "Save" button quickly several times. I don't want to save multiple times.
I want to wait a 500 ms after last press and save only once.

In MobX we have a ready-made reaction 'delay' parameter (check the 101-105 lines):
https://github.com/brianegan/flutter_architecture_samples/blob/master/mobx/lib/stores/todo_store.dart

In a pure Provider architecture I'd use an RxDart debounce() to control saving:

class Store with ChangeNotifier {
final _saveController = StreamController<item>(); //helper stream
final List<Item> _list = []; //data

Store() { _saveController.stream.debounce(ms: 500).listen => // do your save here  }

 void add(item) { //action
 _list.add(item);
 notifyListeners();
  _saveController.add(item); //add event
 }
}

But I have no idea, how to create similar functionality with Async Redux, because it's not recommended to keep streams in the store.

StoreConnector how to handle null returned by the model/converter

I quite often run into this nuisance, that the first thing in builder function is to check if the Model is null, as some values in the AppState are nullable (e.g. after successful login the User's details are requested via an UserGet ReduxAction, before that AppState().self is just null).

return StoreConnector<AppState, User>(
  converter: (store) => store.state.self,
  builder: (context, self) {
    return self == null 
        ? CircularProgressIndicator()
        : CircleAvatar(backgroundImage: CachedNetworkImageProvider(self.imageUrl)),
  }
);

Is this the intended way or should the model or converter function not be able to return null ?

Does this include StateLogger?

In Logging section of document, it has mentioned we can track the states by using a StateLogger() method.

var store = Store<AppState>(
  initialState: state,
  actionObservers: [Log.printer(formatter: Log.verySimpleFormatter)],
  stateObservers: [StateLogger()],  // This
);

However, when I use this, it seems cannot find this method in the package.
Do I need to create one myself or can find in somewhere?

View model does not rebuild immediately after action ends

Hi there,

I have a login flow where I'm dispatching an action (Future) and then immediately trying to access the state (via model) after the action finishes. The strangest thing here is that this happens randomly. Sometimes the value is immediately available

Doing this in the widget:

await widget.vm.login("facebook", result.accessToken.token, "");

if (widget.vm.isLoggedIn == true) {
      _showErrorDialog("Login success",
          "Hello there. We will redirect you to the home page soon");
      Timer(Duration(seconds: 4), () {
        mainNavigator.currentState.pushNamed("/");
      });
    }
    if(widget.vm.isLoggedIn == false) {
      _showErrorDialog("Login fail",
          "Backend once again down 🤦");
    }

Seems that the model rebuilds "too late". I'm making sure that the state is mutated.

Should this scenario work fine?

Abstracted a bit my store setup is:

class AppState {
  final UserState userState;
  final AppEventsState appEventsState;
  final I18nState i18nState;

  AppState({this.userState, this.i18nState, this.appEventsState});

  AppState copy(
      {UserState userState,
      I18nState i18nState,
      AppEventsState appEventsState}) {
    return AppState(
      userState: userState ?? this.userState,
      i18nState: i18nState ?? this.i18nState,
      appEventsState: appEventsState ?? this.appEventsState,
    );
  }

  static AppState initialState() => AppState(
        userState: UserState.initialState(),
        i18nState: I18nState.initialState(),
        appEventsState: AppEventsState.initialState(),
      );

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
          other is AppState &&
              runtimeType == other.runtimeType &&
              userState == other.userState;

  @override
  int get hashCode => userState.hashCode;
}
class UserState {
  final bool loggedIn;
  final String userToken;
  final User user;
  final bool loginFailed;

  UserState({this.loggedIn, this.userToken, this.user, this.loginFailed});

  static UserState initialState() => UserState(
        loggedIn: false,
        userToken: "",
        user: null,
        loginFailed: false
      );

  UserState copy({loggedIn, userToken, user, loginFailed}) => UserState(
        loggedIn: loggedIn ?? this.loggedIn,
        userToken: userToken ?? this.userToken,
        user: user ?? this.user,
        loginFailed: loginFailed ?? this.loginFailed

      );

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
          other is UserState &&
              runtimeType == other.runtimeType &&
              loggedIn == other.loggedIn;

  @override
  int get hashCode => loggedIn.hashCode;
}

Model:

class LoginPageViewModel extends BaseModel<AppState> {
  LoginPageViewModel();

  bool isLoggedIn;
  Function login;

  LoginPageViewModel.build({
    @required this.isLoggedIn,
    @required this.login
  }) : super(equals: [isLoggedIn]);

  @override
  LoginPageViewModel fromStore() {
    return LoginPageViewModel.build(
      isLoggedIn: state.userState.loggedIn,
      login: (authGateway, userToken, uid) {
        return dispatchFuture(UserLoginAction(
          authGateway: authGateway,
          userToken: userToken,
          uid: uid,
        ));
      },
    );
  }
}

also the action



class UserLoginAction extends BaseAction {
  final String authGateway;
  final String userToken;
  final String uid;

  UserLoginAction({this.authGateway, this.userToken, this.uid});

  Preferences _prefs = Preferences.singleton();

  @override
  Future<AppState> reduce() async {
    APIResponse login = await BackboneService()
        .loginUser({"authGateway": authGateway, "userToken": userToken});
    if (login.errorCode != null) {
      return state.copy(userState: userState.copy(loginFailed: true));
    }
    await this._prefs.setLoginToken(login.result["authToken"]);
    return state.copy(
        userState: userState.copy(userToken: login.result["authToken"], loggedIn: true));
  }
}
import 'package:async_redux/async_redux.dart';
import 'package:flutter/material.dart';
import 'package:voyah/store/AppState.dart';

import 'LoginPageView.dart';
import 'LoginPageViewModel.dart';

class LoginPage extends StatelessWidget {
  LoginPage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, LoginPageViewModel>(
      model: LoginPageViewModel(),
      builder: (BuildContext context, LoginPageViewModel vm) => LoginPageView(
        isLoggedIn: vm.isLoggedIn,
        vm: vm
      ),
    );
  }
}

How to return something when an Action is done?

The RefreshIndicator's onRefresh wants a Future Function(), that returned Future must complete when the refresh operation is finished.

Before I would make the call to the API first and then when the response returns I would dispatch an (synchronous) Action and return null.

Now with ReduxAction that can reduce async, I move the API call inside the Action, but what would be the proper way to return something as soon as the Action is finished ?

StoreConnector<AppState, Overview>(
  converter: (store) => store.state.overview,
  builder: (context, overview) {
    return overview == null
      ? Center(child: CircularProgressIndicator())
      : RefreshIndicator(
        onRefresh: () {
          StoreProvider.of<AppState>(context, 'refresh').dispatch(GetOverview());
          return Future.delayed(Duration(seconds: 1));
          /// Before I did this:
          //return ApiService().getOverview().then((Overview ov) {
          //  StoreProvider.of<AppState>(context, 'refresh').dispatch(SetOverview(ov));
          //  return null;
          //}
        },
        child: ListView(...),

Clarifications in README

It would be great if there is a link to the State Declaration section at the top Store and State section. Took me a bit to figure out that the AppState is described further down.

Both listEquals and ListEquality in the State Declaration section are unknowns, although I figured that the former comes from import 'package:flutter/foundation.dart'; and the latter from import 'package:collection/collection.dart';.

There may be more tiny bits like these, but I only stumbled upon these so far.

Would you prefer a PR?

Error in documentation with BaseModel parametrization

Hey there and thanks for this awesome plugin.
I noticed there is an error in section How to provide the ViewModel to the StoreConnector in the main documentation. In the example there, it's stated that BaseModel class should be parametrized with BaseModel<Store<AppState>>. However, in all the other examples its stated that BaseModel<AppState> should be used instead, what actually works.
Nothing too serious but it took me a while to realize why VSCode was complaining about it.

Idea: UndoableAction

I had this thought of maybe having an undo feature in async_redux:

Image a class like this in this package,

abstract class UndoableAction<St> extends ReduxAction<St> {}

you could use in your app like this (for example in a delete action):

class DeleteTodoAction extends UndoableAction<AppState>{
  final int index;
  DeleteTodoAction(this.index);

  @override
  AppState reduce() => state.copyWith(todos: state.todos..removeAt(index));
}

That would after the Action is finished, display a Dialog similar to the UserExceptionDialog with an Undo-Button, that when pressed reverts to the previous state.
Screenshot_20200619-164315_2

I know this could be solved outside of async_redux, but I think this is not something only I come across and it would be awesome to have something like as an easy to use implementation for this Undo feature.
Let me know what you think.

How I can active onDidChange after onInit?

I wanna show a dialog when the page is loaded. But if I call the dialog in onInit, this don't works. If I call the dialog in onDidChange, this don't are called on start of page. But in Flutter StatefulWidget, the didChangeDependencies are called after the page start, and in it I can put the dialog call.

Because that, I will need to put a StatefulWidget only for it. Have any way to do it with storeconnectors?

Thanks.

[Bug] getCurrentNavigatorRouteStack only returns first route

I was working on a PR to add tests for NavigateAction, and I think I discovered a bug in getCurrentNavigatorRouteStack.

/// Trick explained here: https://github.com/flutter/flutter/issues/20451
static List<Route> getCurrentNavigatorRouteStack(BuildContext context) {
List<Route> currentRoutes = [];
Navigator.popUntil(context, (route) {
currentRoutes.add(route);
return true;
});
return currentRoutes;
}

If I understand the code above correctly, it will pop the first route off the stack, and then return true ... which effectively cancels the pop.

This means that only the first route will be added to the getCurrentNavigatorRouteStack. So if you have a navigator route stack with a depth of say 5, getCurrentNavigatorRouteStack will only return the first route from the stack and not the whole thing.

If desired, I can produce a minimal example, just let me know.

Possible Bug: BaseModel.equals not working with null property

I got the following case happening in my application. Whenever the currentTrip property of my state mutates, i get the desired results, my widget is rebuilt to show new information.
The way this variable mutates is through the following action:

class FetchTripAction extends ReduxAction<AppState>{
  @override
  FutureOr<AppState> reduce() async {
    Trip trip;

    // -- Fetch current trip from server
    ApiResponse tripResponse = await ApiServices.get( "/api/current-trip" );

    // -- If response contains trip data, parse it
    if( JsonUtils.isJson( tripResponse.data ) ) {
      // -- Parse trip from received data
      trip = Trip.fromJson( tripResponse.data );
    }

    // -- Create new state. Trip CAN be null if employee is not in trip
    return state.copy(
      currentTrip: trip
    );
  }
}

The problem occurs whenever currentTrip mutates into a null value. My widget is not being rebuilt with new information. It seems to only be the case whenever it is null, if a new trip is fetched from the server with different information, my widget is rebuilt as desired. Is there anything that I am overlooking? Thank you dearly in advance !

This is the class that contains current state of my app

class AppState{
  // -- App vars
  final bool working;
  final bool tripInProgress;
  final bool nearVisitLocation;
  final bool websocketConnected;
  final Position currentPosition;
  final List<BranchPhone> branchPhones;

  // -- Entities
  final Trip currentTrip;
  final Employee deliveryEmployee;

  AppState({this.working, this.currentPosition, this.nearVisitLocation, this.tripInProgress,
    this.deliveryEmployee, this.currentTrip, this.websocketConnected, this.branchPhones});

  AppState copy({bool working, Position currentPosition, List<String> savedUsernames,
    bool nearVisitLocation, bool tripInProgress, Employee deliveryEmployee, Trip currentTrip,
    bool websocketConnected, List<BranchPhone> branchPhones,}) => AppState(
		// --
    branchPhones: branchPhones ?? this.branchPhones,
    currentPosition: currentPosition ?? this.currentPosition,
    currentTrip: currentTrip ?? this.currentTrip,
    deliveryEmployee: deliveryEmployee ?? this.deliveryEmployee,
    nearVisitLocation: nearVisitLocation ?? this.nearVisitLocation,
    tripInProgress: tripInProgress ?? this.tripInProgress,
    websocketConnected: websocketConnected ?? this.websocketConnected,
    working: working ?? this.working,
  );

  static Future<AppState> initialState() async{
    // -- Fetch employee if previously logged in and is persisted
    Employee currentEmployee = await Prefs.getFromJson( Prefs.kEmployeeJson , Employee.fromJson );

    // -- Fetch if currently in trip
    bool tripInProgress = StringUtils.parseBoolean( await Prefs.getString( Prefs.kTripInProgress ) );

    // -- Return app state
    return AppState(
      currentPosition: null,
      currentTrip: null,
      branchPhones: [],
      deliveryEmployee: currentEmployee,
      nearVisitLocation: false,
      tripInProgress: tripInProgress,
      websocketConnected: false,
      working: false,
    );
  }
}

The following is the connector widget

class HomePageConnector extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, HomeViewModel>(
      model: HomeViewModel(),
      onInit: ( Store<AppState> state ) => Hermes.connect(),
      builder: ( BuildContext context, HomeViewModel model ) => HomePage(
        currentTrip: model.currentTrip,
        employee: model.employee,
        branchPhonesCallback: model.branchPhonesCallback,
        logoutCallback: model.logoutCallback,
        startTripCallback: model.startTripCallback,
        websocketConnected: model.websocketConnected,
        working: model.working,
      ),
    );
  }
}

class HomeViewModel extends BaseModel<AppState>{
  // -- Functions and callbacks to pass to view
  Function branchPhonesCallback;
  Function logoutCallback;
  Function startTripCallback;

  // -- Entities and app state to pass to view
  bool working;
  Trip currentTrip;
  Employee employee;
  bool websocketConnected;

  HomeViewModel();
  HomeViewModel.build({
    @required this.branchPhonesCallback,
    @required this.logoutCallback,
    @required this.startTripCallback,
    @required this.currentTrip,
    @required this.employee,
    @required this.websocketConnected,
    @required this.working,
  }) : super(equals: [ currentTrip , employee, websocketConnected, working ]);

  @override
  HomeViewModel fromStore() {
    return HomeViewModel.build(
      branchPhonesCallback: () => dispatch( FetchBranchPhonesAction( employee: store.state.deliveryEmployee ) ),
      logoutCallback: () => dispatch( LogoutAction() ),
      startTripCallback: () => dispatch( StartTripAction( currentTrip: store.state.currentTrip ) ),
      currentTrip: store.state.currentTrip,
      employee: store.state.deliveryEmployee,
      websocketConnected: store.state.websocketConnected,
      working: store.state.working
    );
  }
}

Prevent dispatch of certain action types in tests

I often have actions that dispatch new actions in after* functions, basically chaining a few actions.
When unit testing I always have to mock everything for the whole action chain when I only want to test the first action.
Is there a way to prevent the actual dispatch of these chained action in tests?
StoreTester allows to ignore actions but that is only in regard to their validation.

UserExceptions going to the error queue may not be good for some kinds of test.

Since UserExceptions don't represent bugs in the code, AsyncRedux put them into the store's errors queue, and then swallows them. This is usually what you want during production, where errors from this queue are shown in a dialog to the user. But it may or may not be what you want during tests. In tests there are two possibilities:

  1. You are testing that some UserException is thrown under some situation. For example, you want to test that users are warned if they typed letters in some field that only accepts numbers. To that end, your test would dispatch the appropriate action, and then check if the errors queue now contains the UserException with some specific error message.

  2. You are testing some code that should not throw any exceptions. So if the test throws an exception it means the test should fail, and the exception should show up in the console, for debugging. However, this doesn't work if the test throws an UserException. In this case that exception will simply go to the errors queue and the test will continue running, and may even pass. The only way to make sure is testing that the errors queue is still empty at the end of the test. This is even more problematic if the UserException is thrown inside of a before() method. In this case it will prevent the reducer to run, and the test will probably fail with no errors shown in the console.

Recommendation on how to make the store immutable

Hi,
great library, I think I will use this!

I have one question: In your examples, you make the store immutable by declaring every variable final, implementing hashcode and ==, and providing a copy() method. That's fine, but for a bigger store that's a lot of boilerplate and can lead to mistakes.

Do you recommend using built_value or something similar for the store? Would this have any negative side effects?

NavigateAction.pushNamedAndRemoveUntil

I would like to do a pushNamedAndRemoveUntil but the NavigateAction only have pushNamedAndRemoveAll method.

I think the NavigateAction should mirror the Navigator possibilities and have a pushNamedAndRemoveUntil method.

Pass arguments to a NavigateAction

Flutters Navigator allows one to pass arguments to the new route:

  @optionalTypeArgs
  static Future<T> pushNamed<T extends Object>(
    BuildContext context,
    String routeName, {
    Object arguments,
   }) {
    return Navigator.of(context).pushNamed<T>(routeName, arguments: arguments);
  }

NavigateAction should be able to do this as well.

Is it ok for a

Let's assume I have an AppState like this:

class ListItem {
    final String stuff;
    final int moreStuff;
}

class AppState {
    final List<ListItem> items;
}

Now I want to write a widget, that displays a single ListItem:

class _ViewModel {
    _ViewModel fromStore(...) { 
        // How do I get the ListItem to derive data from?
    }
}

class ListItemWidget extends StatelessWidget {
    ListItemWidget(ListItem listItem);
    final ListItem listItem;
    @override
    Widget build(BuildContext context) {
        return StoreConnector<AppState>(
            mode: _ViewModel
            child: ListTile(/*Stuff based on an instance of ListItem */);
        );
    }
}

Now the problem is: The _ViewModel must be created with a particular ListItem, and just passing the store to it (in fromStore) is not enough because I would not know which ListItem to use. I could write a converter and a second factory function to create the _ViewModel. In that case, I could pass the ListItem to the converter and the factory function. But it seems kind of hacky to me, because I am ignoring the fromStore constructor.

How would you do this?

question - Any similar plugin for React/Angular?

Hey there,
I've been using this plugin for some time and I'm in love with it. I was wondering if the author knows if there is any module for React or Angular which takes advantages of a similar approach to Redux.

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.