Core concepts

Effects

The following is an exploration of what Effects are conceptually and how they are used in Flutter Forge.


Concept

An Effect is conceptually a task that is side-effecting. Another way to think about it is that an Effect is an impure task. Effects are commonly tasks that interact with the file system, or maybe the network, etc.

In Practice

Defining an Effect

Within Flutter Forge we have the EffectTask type to facilitate defining and containing one of these tasks. The following is an example of defining a EffectTask.

In the example below we wrapped the effect in a class named SomeWidgetEffects just to act as a namespace since Dart doesn't provide a namespace feature. This is not required.

class SomeWidgetEffects {
  static final loadNameEffect =
      EffectTask<State, Environment, LoadOnComponentInitAction>(
          (state, environment, context) async {
    final name = await environment.getName();
    return ObtainedName(name);
  });
}

In the above example we have a class called SomeWidgetEffects. This only exists to provide a namespace as Dart doesn't support the concept of a namespace. Then inside that we have defined an Effect named loadNameEffect. This is the actual EffectTask. Inside the EffectTask we can see that it is calling an async method named getName on the injected environment and then returning an Action named ObtainedName along with the associated name. Note: Effects don't have to return an Action. It is actually pretty common to have what we think of as Fire and Forget Effects.

Triggering Effects

The other place you will interact with Effects in Flutter Forge is when you trigger them. This is done via Actions and the Reducer. When you handle an Action in the Reducer you return a ReducerTuple containing the new State as well as an Array of EffectTasks.

final someComponentReducer =
    Reducer<SomeWidgetState, SomeWidgetEnvironment, SomeWidgetAction>(
        (state, action) {
  switch (action) {
    case Load _:
      return ReducerTuple(
        SomeWidgetState(count: state.count, name: "Loading..."), [SomeWidgetEffects.loadNameEffect]);
  }
});

The above is a simple example where we return the loadNameEffect as part of the ReducerTuple in the Reducer. The Store takes the ReducerTuple and processes the State and Effects.

Passing Data to Effects

Sometimes you want to pass data that the Reducer received via an Action on to an Effect. This is done by creating what we call an Effect builder. It is nothing more than a function that builds and returns the EffectTask.

The following is an example of this with the associated contextual elements.

// Actions
sealed class LoginAction implements ReducerAction {}

class LoginButtonTapped extends LoginAction {
  LoginButtonTapped({required this.email, required this.password});

  final String email;
  final String password;
}

class LoginSucceeded extends LoginAction {
  LoginSucceeded(this.token);
  final String token;
}

class LoginFailed extends LoginAction {
  LoginFailed(this.error, this.stacktrace);
  final Object error;
  final StackTrace stacktrace;
}

// Effects
class LoginEffects {
  static EffectTask<LoginState, LoginEnvironment, LoginAction> login({
    required String email,
    required String password,
  }) {
    return EffectTask<LoginState, LoginEnvironment, LoginAction>(
        (state, environment, context) async {
      try {
        final token = await environment.login(email, password);
        return LoginSucceeded(token);
      } catch (e, stacktrace) {
        return LoginFailed(e, stacktrace);
      }
    });
  }
}

// Reducer
final loginReducer =
    Reducer<LoginState, LoginEnvironment, LoginAction>((state, action) {
  switch (action) {
    case LoginButtonTapped _:
      return ReducerTuple(
        const LoginState(submissionState: AsyncState.loading()),
        [LoginEffects.login(email: action.email, password: action.password)],
      );
    case LoginSucceeded _:
      return ReducerTuple(
        LoginState(submissionState: AsyncState.data(action.token)),
        [],
      );
    case LoginFailed _:
      return ReducerTuple(
        LoginState(
          submissionState: AsyncState.error(action.error, action.stacktrace),
        ),
        [],
      );
  }
});

In the above we can see that we defined a static method called login() within the LoginEffects class. This method is responsible for building a EffectTask using the email and password that are passed into it.

Then within the Reducer we are able to simply call it to build the EffectTask we are looking for bound to the specific email and password as follows.

LoginEffects.login(email: action.email, password: action.password)

This enables us to easily pass data from Actions to Effects.

Previous
Reducer