Core concepts

Component

In Flutter Forge the Component is the base of modular UI modeling. Therefore, it is critical that you understand both conceptually and technically what a Component is in the eyes of Flutter Forge.


Conceptually

Conceptually a Component is a modular piece of a Flutter application that is used to wrap up the UI concerns & business logic concerns in a modular, testable, and composable manner.

In Practice

In practice, you can think of this as a bundling up of State, Environment, Actions, Effects, a Reducer, and a Widget to tie them all together through a central component called the Store.

Technical Perspective

From a technical standpoint Flutter Forge accomplishes this by having all components define a consistent and complete interface. This is what makes it possible to compose components together into greater components. It is also what facilitates the ease of testing, and being able to build components in isolation.

This formal and complete interface is defined through the fact that each component Widget constructor must take in a Store. The Store is really the key piece of the puzzle, as it is this formal & complete interface. The Store is responsible for managing the change of state over time in a controlled way and facilitating observability of that state change as well as composability. To accomplish this it depends on three generic types State, Environment, and Action. These generic types must be concretized through type parameterization to define a complete formal interface for a Store, and in turn a component.

Once we have a complete formal interface defined by the Store we can create the Widget and the Reducer to round out our component.

Example

If we look at the following definition of a Component we can see these various pieces in play.

First we define a State class to formally represent the state that this Component will care about.

@immutable
class CounterWidgetState extends Equatable {
  const CounterWidgetState({required this.count});

  final int count;

  @override
  List<Object> get props => [count];
}

Then we define an Environment class to formally define an interface for the dependency injection that the Component cares about.

class CounterWidgetEnvironment {
  const CounterWidgetEnvironment();
}

After that we define our Actions classes to formally define which user interactions the Store can know about and which Effect results the Store can know about.

sealed class CounterWidgetAction implements ReducerAction {}

class CounterWidgetIncrementButtonTapped extends CounterWidgetAction {}

At this point we have completed the formal interface defining such that we can concretize a Store for the component. Luckily the ComponentWidget provided by Flutter Forge makes this easy, as it allows us to concretize the ComponentWidget with the State, Environment, and Base Action. In turn it concretizes the Store that is required by the constructor.

class CounterWidget extends ComponentWidget<CounterWidgetState,
    CounterWidgetEnvironment, CounterWidgetAction> {
  const CounterWidget({super.key, required super.store});

  @override
  Widget build(context, viewStore) {
    return Column(
      children: [
        Rebuilder(
            store: store,
            builder: (context, state, child) {
              return Text('${state.count}');
            }),
        TextButton(
            onPressed: () {
              viewStore.send(CounterWidgetIncrementButtonTapped());
            },
            child: const Text('increment'))
      ],
    );
  }
}

Last but not least. We define a Reducer and any Effects to provide the pure state management logic as well as the impure logic.

final counterReducer =
    Reducer<CounterWidgetState, CounterWidgetEnvironment, CounterWidgetAction>(
        (state, action) {
  switch (action) {
  	case CounterWidgetIncrementButtonTapped _:
    	return ReducerTuple(CounterWidgetState(count: state.count + 1), []);
  }
});

This Reducer can then be used when constructing this Widget's associated Store.

Previous
Core Tenets