Guides

Interacting with External Systems

This is a quick exploration of how you interact with External Systems within Flutter Forge.


In general, you are probably thinking. What do you mean interacting with external systems? Normally, I just write some code, and I am good.

That is because most libraries don't provide any mechanism to help facilitate separating external systems interaction or non-pure/effectful code from pure code. This is important to make testing easy as well as simply from an architectural standpoint.

Flutter Forge addresses this by providing two main concepts to aid with this, effects and environment. The effects facilitate formalizing the separation of effectful code from pure code. While the environment facilitates formalizing dependency injection of external dependencies.

An Example

Let's walk through this in a bit more detail with some code to make things a bit clearer. Specifically, let's explore what it would look like if we were building a component that houses some form fields and has a button to submit the values in the form fields to some external system, e.g. a REST/GraphQL/etc. API.

The Button & Action

First let's assume that we have the following state,

enum SubmissionStatus { pending, submitting, succeeded, failed }

@immutable
class CreatePostComponentState extends Equatable {
  const CreatePostComponentState(
      {required this.message, required this.postSubmissionStatus});

  final String message;
  final SubmissionStatus postSubmissionStatus;

  @override
  List<Object> get props => [message, postSubmissionStatus];
}

and the following actions setup.

sealed class CreatePostComponentAction implements ReducerAction {}

class CreatePostComponentSubmitButtonTapped
    implements CreatePostComponentAction {}

class CreatePostComponentSuccessfullyPosted
    implements CreatePostComponentAction {}

class CreatePostComponentFailedToPost implements CreatePostComponentAction {}

Triggering an Effect

When the button is pressed we would simply call viewStore.send(CreatePostComponentSubmitButtonTapped()) in the widget.

Then our reducer would need to handle that action. But what do we want it to actually do? Well, I think we want to update the status from pending to submitting so that the UI can be updated in the widget. But I also think we want to actually create the post by interacting with the external system.

In Flutter Forge we have to do this by triggering an effect through our reducer. This reducer's action handling might look as follows.

  switch (action) {
    case CreatePostComponentSubmitButtonTapped _:
      return ReducerTuple(
        CreatePostComponentState(
            message: state.message,
            postSubmissionStatus: SubmissionStatus.submitting),
        [CreatePostComponentEffects.createPost]);
    case CreatePostComponentSuccessfullyPosted _:
      return ReducerTuple(
        CreatePostComponentState(
            message: state.message,
            postSubmissionStatus: SubmissionStatus.succeeded),
        []);
    case CreatePostComponentFailedToPost _:
      return ReducerTuple(
        CreatePostComponentState(
            message: state.message,
            postSubmissionStatus: SubmissionStatus.failed),
        []);
  }

In the above we can see that it includes the CreatePostComponentEffects.createPost effect in the array of effects that are returned as part of the ReducerTuple. The ReducerTuple makes its way back to the store, and it handles executing the effect and feeding any actions it produces back into the store.

So all we have to do is define the effect and then make sure that our reducer handles an action that sends it to the store.

Defining an Effect

So now that we understand how the effects get triggered. How do we define them? Simple just do something like the following.

class CreatePostComponentEffects {
  static final createPost = EffectTask<
      CreatePostComponentState,
      CreatePostComponentEnvironment,
      CreatePostComponentAction>((state, environment, context) async {
    try {
      await environment.createPost(state.message);
      return CreatePostComponentSuccessfullyPosted();
    } catch (e) {
      return CreatePostComponentFailedToPost();
    }
  });
}

In the above you can see that we have defined the createPost effect and that all it really does is farm out the creation of the post to a createPost function that was dependency injected via the environment. Then based on success or failure it returns an appropriate action back into the store so that the reducer can then appropriately update state, and in turn visually update the component.

That is it! Just define an effect, using environment dependencies appropriately, and wire them up with your reducer. You may have noticed, this approach relegates the effectful (a.k.a. non-pure) code out to either the effect itself or even better a dependency injected function/service that does the external system interaction. This characteristic is one of the core characteristics that significantly aids with good architecture as well as ease of testing.

Previous
Process for building a Component