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
230 stars 41 forks source link

Devtools for AsyncRedux #85

Closed daniellam258 closed 4 years ago

daniellam258 commented 4 years ago

When making decision about choosing among State Management library, Devtools is one of the main factors that devs will interest. I wonder whether we can use Redux Dart's Devtools for AsyncRedux too or we have to creating one. Either way, I think documentation about this subject would be great.

marcglasberg commented 4 years ago

What do you mean by "Redux Dart's Devtools"? You mean: https://pub.dev/packages/redux_dev_tools or https://pub.dev/packages/flutter_redux_dev_tools or https://pub.dev/packages/redux_remote_devtools or something else entirely?

gadfly361 commented 4 years ago

@lhdung258 I have used redux_remote_devtools with async_redux. If that it the one you mean, I can dig up how I did it (took a little tweaking).

dennis-tra commented 4 years ago

@gadfly361 I would be really interested in how you did it. If you don't mind I would really appreciate a brief description of your setup.

gadfly361 commented 4 years ago

@dennis-tra Here is a minimal example:

Create a flutter app

flutter create async_redux_devtools
cd async_redux_devtools

open pubspec.yaml and add the following dependencies

async_redux: ^3.0.5
redux_remote_devtools: ^2.0.0

download flutter packages and install removedev-server

flutter packages get
npm install -g remotedev-server

replace lib/main.dart with the following

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

// This is the async_redux 'store' that our app cares about
Store<int> store;

// This is a 'devtools store' used to wire up redux remote devtools, but our app doesn't actually care about this store
DevToolsStore<int> devToolsStore;

// This observes changes to the 'store' and dispatches an event using the 'devtools store' so it is picked up by redux remote devtools
class DevToolsStateObserver extends StateObserver {
  @override
  void observe(_reduxAction, _stateIni, stateEnd, _dispatchCount) {
    devToolsStore.dispatch(_reduxAction.runtimeType.toString());
  }
}

void main() async {
  // Initializing the 'store' our app cares about
  store = Store<int>(
    initialState: 0,
    stateObservers: <StateObserver>[
      DevToolsStateObserver(),
    ],
  );

  // Initializing the 'devtools store' that our app doesn't actually care about
  final RemoteDevToolsMiddleware<int> remoteDevtools =
      RemoteDevToolsMiddleware<int>('localhost:8000');
  await remoteDevtools.connect();

  devToolsStore = DevToolsStore<int>(
    // Note: we are returning the state of the 'store' our app cares about
    // for any event dispatched from the 'devtools store' that our app doesn't actually care about
    (int _, dynamic action) => store.state,
    initialState: 0,
    middleware: [remoteDevtools],
  );
  remoteDevtools.store = devToolsStore;

  // running the app
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => StoreProvider<int>(
      store: store,
      child: MaterialApp(
        home: MyHomePageConnector(),
      ));
}

class IncrementAction extends ReduxAction<int> {
  @override
  int reduce() => state + 1;
}

class DecrementAction extends ReduxAction<int> {
  @override
  int reduce() => state - 1;
}

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

  @override
  Widget build(BuildContext context) {
    return StoreConnector<int, ViewModel>(
      model: ViewModel(),
      builder: (BuildContext context, ViewModel vm) => MyHomePage(
        counter: vm.counter,
        onIncrement: vm.onIncrement,
        onDecrement: vm.onDecrement,
      ),
    );
  }
}

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

  int counter;
  VoidCallback onIncrement;
  VoidCallback onDecrement;

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

  @override
  ViewModel fromStore() => ViewModel.build(
        counter: state,
        onIncrement: () => dispatch(IncrementAction()),
        onDecrement: () => dispatch(DecrementAction()),
      );
}

class MyHomePage extends StatelessWidget {
  final int counter;
  final VoidCallback onIncrement;
  final VoidCallback onDecrement;

  MyHomePage({
    @required this.counter,
    @required this.onIncrement,
    @required this.onDecrement,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Current count: $counter',
                style: const TextStyle(fontSize: 30)),
            RaisedButton(
              child: Text('Increment'),
              onPressed: onIncrement,
            ),
            RaisedButton(
              child: Text('Decrement'),
              onPressed: onDecrement,
            )
          ],
        ),
      ),
    );
  }
}

run devtools

remotedev --port 8000

run app

flutter run

go to localhost:8000 in a browser

ezgif com-video-to-gif

Caveats

catalunha commented 4 years ago

Congratulations on the detailed explanation. AsyncRedux is very import for my work in class.

dennis-tra commented 4 years ago

Wow, thanks for your thorough writeup @gadfly361 !

In the meantime, I gave it a try as well and I may propose a slightly different solution. If I see it right you don't need the DevToolsStore at all if you don't support dispatching actions from the remote dev tools. In the remote dev tools library are in all necessary places store == null checks to prevent NPEs, so it's safe to leave it null for the RemoteDevToolsMiddleware.

I just copied your example from above and changed some bits to reflect my idea of an alternative solution. I'd like to point out though, that I haven't tested the below code. It's working for my setup so far.

import 'package:async_redux/async_redux.dart';
import 'package:flutter/material.dart';
import 'package:redux/redux.dart' as Redux; // import vanilla redux
import 'package:redux_dev_tools/redux_dev_tools.dart';
import 'package:redux_remote_devtools/redux_remote_devtools.dart';

// This is the async_redux 'store' that our app cares about
Store<int> store;

final devToolsActionObserver = DevToolsActionObserver();

class DevToolsActionObserver extends ActionObserver<AppState> {
  // The store that our app cares about
  Store<int> store;

  final middleware = RemoteDevToolsMiddleware<AppState>('localhost:8000');

  @override
  void observe(ReduxAction<int> action, int dispatchCount, {bool ini}) {
    // You may want to add a check for `ini` here to prevent two actions appearing in the remote dev tools UI
    if (store == null) {
      return;
    }
    // Judging from the code, instantiating a new redux store for every action has a negligible performance impact.
    // It would be nicer to be able to hand in `this.store` directly which would be possible because inside `call`
    // only `store.state` is accessed which is also present on the async_redux store.
    final reduxStore = Redux.Store<int>(null, initialState: this.store.state);
    this.middleware.call(reduxStore, action, (action) => null);
  }
}

void main() async {
  // Initializing the 'store' our app cares about
  store = Store<int>(
    initialState: 0,
    actionObservers: <ActionObserver>[devToolsActionObserver],
  );
  // Wire up the store to our action observer
  devToolsActionObserver.store = store;
  await devToolsActionObserver.middleware.connect();

  // running the app
  runApp(MyApp());
}
gadfly361 commented 4 years ago

@dennis-tra Awesome, thanks for sharing! I think since this is just some glue code, whatever works for you / others sounds good to me 👍