Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do I dispatch actions to the Store without building a widget? #51

Closed
jeroen-meijer opened this issue May 19, 2018 · 12 comments
Closed

Comments

@jeroen-meijer
Copy link

jeroen-meijer commented May 19, 2018

Hi there.

How was your trip? 😄

I've been happily using flutter_redux, but I'm having trouble with one thing in particular.
I want some methods inside my Widgets to dispatch actions to the Store from the StoreProvider, but not necessarily build a widget.

Since StoreConnector() is only intended for passing the store to a Widget in the context of the build method, I'm wondering: is there any class or function I can use inside of a method that returns void (for example) that just provides me with the Store, so I can dispatch an action to it without needing to build a widget?

Thank you.

@brianegan
Copy link
Owner

Hey hey :) Trip was great, thanks! And apologies -- somehow missed this one!

Overall, I'm a bit confused by this line, so my advice not be amazing: "I want some methods inside my Widgets to dispatch actions to the Store from the StoreProvider, but not necessarily build a widget." If you've got methods inside a Widget, I guess I'm thinking you'd need to instantiate / build it somewhere?

Here are a couple options I could think of:

  1. Use the StoreConverter to create a ViewModel that contains functions that dispatch actions to the store for ya. For example, you can create a StoreConnector with a converter that returns a Function(). You can then pass this function to the Widget that needs to dispatch an action. Examples can be seen here:
    - Only returns a function that dispatches an action: https://github.com/brianegan/flutter_architecture_samples/blob/master/example/redux/lib/containers/add_todo.dart
    - Returns a ViewModel, with some data AND a function that dispatches an action: https://github.com/brianegan/flutter_architecture_samples/blob/master/example/redux/lib/containers/todo_details.dart
  2. Pass Store as a constructor property directly to the Widget which has methods that need to call the Store. Something like: MyWidget(store: StoreProvider.of<AppState>()). Then you can use the store within the methods of that class or it's State class.
  3. Create a Singleton that holds the Store and provides static access to the Store (note, this might not be the best idea, as it couples your Widget to the Singleton and can make testing harder). You can follow this example of how to create a Singleton that could hold the Store: https://stackoverflow.com/questions/12649573/how-do-you-build-a-singleton-in-dart

Do any of those help? If not, please feel free to write back, and it'd be great if you can describe a use-case, and I'll try to help further :)

@jeroen-meijer
Copy link
Author

Thank you for your quick response :)

I'll look through the links you've provided. Your second idea especially seems like it would solve my problem, but then again; passing the store to a widget might break one of the key features of redux (as you've mentioned in your DartConf 2018 talk) which is flexibility in moving classes or UI elements around and keeping things loosely coupled. In the meantime, I'll give you a use-case to further clarify my situation.

Say I have a Widget that displays a button. Pressing it will then trigger a function that calls asks the database for a variable and then assigns a field of my AppState to that variable. I'm using pseudocode for the database part here.

import 'package:flutter/material.dart';

// Temporary variables for demonstration purposes.
dynamic Database;
class AssignUserByDocument {AssignUserByDocument(dynamic document);}
dynamic ExampleSpecialStoreProvider;

class HomeScreen extends StatelessWidget {

  void _handleButtonPressed() async {
    // Make an async call to the example database.
    var document = await Database.getDocument("user").byId("1234");
    // This is where I want to dispatch an action to the store
    // (but not build a widget, so I can't use StoreConnector, right?).
    // Example solution:
    ExampleSpecialStoreProvider(
      converter: (store) => store,
      action: (store) {
        store.dispatch(new AssignUserByDocument(document));
      }
    );
  }

  @override
  Widget build(BuildContext context) {
return Scaffold(
        body: Center(
      child: MaterialButton(
        onPressed: _handleButtonPressed,
        child: Text("Initialize user object from DB!"),
      ),
    ));
  }
}

Now, it doesn't really matter if the _handleButtonPressed is async or not for what I'm asking (I've already found a way to do async action dispatching using redux_thunk), but I want to have a way to access the closest neighbouring store and dispatch an action, but, as you can see, without actually building a widget - a way to access the store without a front-end, so to speak.

I hope this is clarifying things for you.
The solution I'm using now is a simple class with static variables that are accessible from anywhere (I believe that's what a singleton actually is, correct?) but I'd like to do it through redux.

@brianegan
Copy link
Owner

brianegan commented May 21, 2018

Thanks @jeroen-meijer, that definitely helps me understand.

In this case, I'd actually recommend a slightly different path: Middleware functions should generally handle async calls in Redux. More docs: https://github.com/johnpryan/redux.dart/blob/master/doc/async.md

In this case, you'll want to create a Middleware function that will listen forLoadUserActions that are dispatched. When a LoadUserAction is dispatched, it will call the database layer for ya, and transform the result of that async call into new success / fail actions.

You can provide a function to your HomeScreen that will dispatch a LoadUserAction using a StoreConnector.

(Also, random side fact: your ExampleSpecialStoreProvider is the same thing as a StoreBuilder provided by this lib if you ever need that functionality again!).

Example adapted to use Middleware:

// Actions
class LoadUserSuccessAction {AssignUserByDocument(dynamic document);}
class LoadUserFailedAction {}

// Also Introduce Loading Action
class LoadUserAction(
  final String id;
  LoadUserAction(this.id);
);

// Create a Middleware
void loadUserMiddleware(Store<AppState> store, dynamic action, NextDispatcher next) {
  if (action is LoadUserAction) {
    Database.getDocument("user").byId(action.id)
      .then((document) => store.dispatch(LoadUserSuccessAction(document))
      .catchError((error) => store.dispatch(LoadUserFailedAction());
  }

  next(action);
}

AppState exampleReducer(AppState prev, dynamic action) { return prev; }

// Create your store with your middleware
final store = new Store<AppState>(exampleReducer, middleware: [loadUserMiddleware]);

class HomeScreen extends StatelessWidget {
  final Function() onButtonPressed;

  HomeScreen({Key key, @required this.onButtonPressed}): super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: MaterialButton(
          onPressed: onButtonPressed,
          child: Text("Initialize user object from DB!"),
        ),
      ),
    );
  }
}

// When instantiating the HomeScreen...
StoreConnector<AppState, Function()>(
  converter: (store) => store.dispatch(LoadUserAction("12345")),
  builder: (context, fn) => HomeScreen(onButtonPressed: fn),
);

Hope that helps!

@brianegan
Copy link
Owner

@jeroen-meijer Did I totally blow your mind, or is that working out for ya? Let me know, or if it's safe to close this out now :)

@brianegan
Copy link
Owner

Gonna go ahead and close this out, please let me know if you need any further help :)

@hereisderek
Copy link

hereisderek commented Sep 21, 2018

on a similar question, how to I get store instance outside of a widget scope? currently I just save the store instance as a global static variable, however this approach feel very much like a hack. to explain the usage, consider I have a local service like component (one that listen to app lifecycle or network connectivity), which I'd like to have the ability to actively dispatch events such as onAppBackground etc.
currently for me it's like this:

Future<Store<AppState>> createStore({BuildConfig buildConfig}) async {
  initLog();

  final SharedPreferences   _prefs          =   await SharedPreferences.getInstance();
  final Database            _database       =   DatabaseImpl(_buildConfig.apiPoint);
  final Recorder            _recorder       =   RecorderImpl.initialize(database: _database);

  final Store<AppState> store = Store(
    appReducer,
    initialState: AppState.initial(buildConfig: _buildConfig, recorder: _recorder),
    distinct: true,
    middleware: [
      MiddlewareTest(),
      RecordEntityMiddleware(database: _database),
    ],
  );
  
  **_recorder.store = store;**
  return store;
}

not exactly as a global static variable but hacky nonetheless (or is it?)

@rsanath
Copy link

rsanath commented Jun 12, 2019

on a similar question, how to I get store instance outside of a widget scope? currently I just save the store instance as a global static variable, however this approach feel very much like a hack. to explain the usage, consider I have a local service like component (one that listen to app lifecycle or network connectivity), which I'd like to have the ability to actively dispatch events such as onAppBackground etc.
currently for me it's like this:

Future<Store<AppState>> createStore({BuildConfig buildConfig}) async {
  initLog();

  final SharedPreferences   _prefs          =   await SharedPreferences.getInstance();
  final Database            _database       =   DatabaseImpl(_buildConfig.apiPoint);
  final Recorder            _recorder       =   RecorderImpl.initialize(database: _database);

  final Store<AppState> store = Store(
    appReducer,
    initialState: AppState.initial(buildConfig: _buildConfig, recorder: _recorder),
    distinct: true,
    middleware: [
      MiddlewareTest(),
      RecordEntityMiddleware(database: _database),
    ],
  );
  
  **_recorder.store = store;**
  return store;
}

not exactly as a global static variable but hacky nonetheless (or is it?)

I am having a very similar situation where my app listens to a child in my firebase database.
I attach the listener in the initState of my HomeScreen.
But how do I dispatch an action when I get a response?

class _HomeScreenState extends State<HomeScreen> {
  
    @override
    Widget build(BuildContext context) {
      return StoreConnector<AppState, VoidCallback>(
        converter: // ??? 
        builder: (BuildContext context, VoidCallback callback) {
          return Scaffold(
            appBar: AppBar(title: Text(widget.title)),
            body: SomeComponentThatWillDisplayTheResults()
          );
        },
      );
    }
  
    _onValueChange(Event event) {
        // How do I dispatch an action now ??
    }
  
    @override
    void initState() {
      super.initState();
  
      Firebase.database
          .reference()
          .child("counter")
          .onValue
          .listen(_onValueChange);
    }
 }

@JuanGamarraDev
Copy link

My favorite solution.

void _onEditNote() { final store = StoreProvider.of<AppState>(context); store.dispatch(NoteEditAction(note)); }

@rsanath
Copy link

rsanath commented Jun 14, 2019

My favorite solution.

void _onEditNote() { final store = StoreProvider.of<AppState>(context); store.dispatch(NoteEditAction(note)); }

Yes, I had forgotten that StoreProvider is an InheritedWidget.
This is the correct way to access the store from within a Widget class.

@zaun
Copy link

zaun commented Jun 29, 2019

@ramsanath @JuanGamarraDev How do I do this without context?

  initState() {
    super.initState();
    SchedulerBinding.instance.addPostFrameCallback((_) {
      runInitTasks();
    });
  }

  @protected
  Future runInitTasks() async {
     final store = StoreProvider.of<AppState>(context); // How do I get context??
     store.dispatch(connectDatabase);
  }

@zaun
Copy link

zaun commented Jun 29, 2019

Nevermind. It's global in stateful widgets. I'm an idiot today I guess.

@VenturaLorenzo
Copy link

Is there a difference between using
StoreProvider.of<AppState>(context).disparch(action); on the target widget (for example a button)
and passing to the widget a Function(), build in the ViewModel of the StoreCoverter, that dispatch the action for it?

I mean, is pretty obvious that the simpler solution is to just write the single line of code that take the store from the StoreProvider and dispatch the action but I saw in the flutter Redux Architecture Sample that they prefer to pass as a paramenter a Function that dispatch the action. Is there any difference? using StoreProvider "costs" more that just building a function in the StoreConverter ? maybebecause we already have the store in the scope in the second case?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants