Build & Test codecov License: MIT

Hew

Hew is a simple and fast library that helps you organize your Flutter app's presentation layer.

It's inspired by StateNotifier from Riverpod and Cubit from bloc, and introduces two new entities: Presenter and PresentationModel.

It's worth noting that Cubit is designed to be used in the domain layer, whereas hew is meant to be used exclusively in the presentation layer.

The goal of this library is to provide a convenient way to split StatefulWidgets into three distinct parts: Model, Presenter and Widget. By doing so, you can easily test your Presenter and Model without having to worry about Widget itself. Additionally, it separates your presentation logic from your view logic and makes your code easier to maintain.

Configuring

Add hew to your pubspec.yaml file:

dependencies:
  hew: ^0.0.1

Usage

Presenter

Presenter is a class that contains all the logic of your widget.

The presenter is responsible for updating a model of the widget, and the widget is responsible for displaying the model.

Let's look at the counter presenter example:

class CounterPresenter extends Presenter<CounterModel> {
  CounterPresenter() : super(CounterModel());

  void onIncreaseCounterTap() => notify(() => model.counter++);
}

Here we have a simple presenter that creates CounterModel and increases counter by one on onIncreaseCounterTap call.

Model

Model is a class that contains all data we want to be displayed in the widget.

Let's create a simple model for our previous presenter:

class CounterModel with MutableEquatableMixin {
  int counter = 0;

  @override
  List<Object?> get mutableProps => [counter];
}

The model contains only one field - counter, the value we want to display.

It also mixes in MutableEquatableMixin and overrides mutableProps getter.
You should pass all your mutable fields to mutableProps getter to make Presenter behave correctly.

MutableEquatableMixin is a mixin that adds mutableHashCode to the model. This is an integer generated based on hashCodes of fields you pass to mutableProps.

Presenter uses mutableHashCode to make notify method notify its listeners only if there're any changes in model.

Widget

Now it's time to display our model.

We can do this by using PresenterWidget, PresenterStatefulWidget or by using PresenterBuilder.

Let's look how the first approach works:

class Counter extends PresenterWidget<CounterPresenter, CounterModel> {
  const Counter({Key? key}) : super(key: key);

  @override
  CounterPresenter createPresenter() => CounterPresenter();

  @override
  Widget build(context, presenter, model) {
    return TextButton(
      onPressed: presenter.onIncreaseCounterTap,
      child: Text(model.counter.toString()),
    );
  }
}

Here we display the counter value inside TextButton and call onIncreaseCounterTap on onPressed event.
PresenterWidget maintains Presenter lifecycle and supports automatic rebuilding on model change.

But sometimes we need to implement something more complex and in flutter we usually do it by using StatefulWidget. Hew has PresenterStatefulWidget for this:

class Counter extends PresenterStatefulWidget<CounterPresenter> {
  const Counter({super.key});

  @override
  CounterPresenter createPresenter() => CounterPresenter();

  @override
  PresenterState createState() => _CounterState();
}

class _CounterState
    extends PresenterState<CounterPresenter, CounterModel, Counter> {
  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: presenter.onIncreaseCounterTap,
      child: Text(model.counter.toString()),
    );
  }
}

And if you want to be more flexible, you can use PresenterBuilder:

class Counter extends StatelessWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return PresenterBuilder<CounterPresenter, CounterModel>(
      resolver: () => CounterPresenter(),
      builder: (context, presenter, model) => TextButton(
        onPressed: presenter.onIncreaseCounterTap,
        child: Text(model.counter.toString()),
      ),
    );
  }
}

ModelObserver

All previous approaches by default automatically rebuild themself whenever the model changes. If you want to handle state changes manually, you can disable this behaviour and use ModelObserver.

To disable automatic rebuilding, you can pass false to rebuildOnChanges parameter of PresenterBuilder, or override rebuildOnChanges getter in your PresenterWidget and PresenterStatefulWidget:

class Counter extends PresenterWidget<CounterPresenter> {
  bool get rebuildOnChanges => false;
  ...
}

// or

@override
Widget build(BuildContext context) {
  return PresenterBuilder<CounterPresenter, CounterModel>(
    rebuildOnChanges: false,
    resolver: () => CounterPresenter(),
    builder: (context, presenter, model) => TextButton(
      onPressed: presenter.onIncreaseCounterTap,
      child: Text(model.counter.toString()),
    ),
  );
}

Then you should use ModelObserver to track model changes:

@override
Widget build(context, presenter, model) {
  return ModelObserver(
    presenter: presenter,
    builder: (context) => TextButton(
      onPressed: presenter.onIncreaseCounterATap,
      child: Text(model.counter.toString()),
    ),
  );
}

If you want to rebuild your widget only when changes are in some specific field, you can use when parameter, just specify a path to the field:
when: (model) => model.counterB.

If there are multiple fields, you can pass a list of fields as well:
when: (model) => [model.counterA, model.counterB].

Expansions

TBD

Libraries

hew
rx