puer_flutter 1.0.0 copy "puer_flutter: ^1.0.0" to clipboard
puer_flutter: ^1.0.0 copied to clipboard

A bunch of widgets that helps you to easily works with puer with Flutter.

Puer Flutter #

Puer

Pub CI License: MIT


Flutter widgets for puer — a reactive, functional state management library based on The Elm Architecture.

This package provides five essential widgets to integrate your puer features into Flutter apps:

  • FeatureProvider — Exposes a feature to the widget tree
  • FeatureBuilder — Rebuilds UI when state changes
  • FeatureListener — Executes side effects in response to state changes
  • FeatureSelector — Rebuilds UI only when a specific part of state changes
  • FeatureEffectListener — Handles effects in the UI layer (navigation, dialogs, etc.)

Installation #

Add puer_flutter to your pubspec.yaml:

dependencies:
  puer_flutter: ^1.0.0

Note: puer_flutter re-exports the core puer package, so you only need this single dependency.


Quick Example #

import 'package:flutter/material.dart';
import 'package:puer_flutter/puer_flutter.dart';

// Your feature types
typedef CounterFeature = Feature<CounterState, CounterMessage, CounterEffect>;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: FeatureProvider<CounterFeature>(
        create: (context) => Feature<CounterState, CounterMessage, CounterEffect>(
          initialState: const CounterState(count: 0),
          update: counterUpdate,
        ),
        child: const CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    final feature = FeatureProvider.of<CounterFeature>(context);

    return Scaffold(
      body: Center(
        child: FeatureBuilder<CounterFeature, CounterState>(
          builder: (context, state) => Text(
            '${state.count}',
            style: Theme.of(context).textTheme.displayLarge,
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => feature.add(Increment()),
        child: const Icon(Icons.add),
      ),
    );
  }
}

Widgets #

1. FeatureProvider #

Purpose: Provides a Feature instance to the widget tree and manages its lifecycle.

When to use: Wrap your app or screen in a FeatureProvider to make a feature available to descendant widgets.

Usage

Create mode — Creates and initializes a feature:

FeatureProvider<CounterFeature>(
  create: (context) => Feature<CounterState, CounterMessage, CounterEffect>(
    initialState: const CounterState(count: 0),
    update: counterUpdate,
  ),
  child: const MyWidget(),
)

Value mode — Provides an existing feature:

final feature = Feature<CounterState, CounterMessage, CounterEffect>(
  initialState: const CounterState(count: 0),
  update: counterUpdate,
);

FeatureProvider<CounterFeature>.value(
  value: feature,
  child: const MyWidget(),
)

Retrieve the feature:

final feature = FeatureProvider.of<CounterFeature>(context);
feature.add(Increment());

Lifecycle

  • Create mode: Automatically calls feature.init() when the widget enters the tree and feature.dispose() when it leaves
  • Value mode: Does not manage lifecycle — you must call init() and dispose() manually

2. FeatureBuilder #

Purpose: Rebuilds UI when the feature's state changes.

When to use: When you need to display state values in your UI.

Usage

FeatureBuilder<CounterFeature, CounterState>(
  builder: (context, state) {
    return Text('Count: ${state.count}');
  },
)

With custom feature instance:

FeatureBuilder<CounterFeature, CounterState>(
  feature: myFeatureInstance,
  builder: (context, state) {
    return Text('Count: ${state.count}');
  },
)

Filter rebuilds with buildWhen:

FeatureBuilder<CounterFeature, CounterState>(
  buildWhen: (previous, current) {
    // Only rebuild when count is even
    return current.count % 2 == 0;
  },
  builder: (context, state) {
    return Text('Even count: ${state.count}');
  },
)

3. FeatureListener #

Purpose: Executes side effects (navigation, dialogs, snackbars) in response to state changes without rebuilding UI.

When to use: When you need to perform one-time actions based on state changes, not display state values.

Usage

FeatureListener<AuthFeature, AuthState>(
  listener: (context, state) {
    if (state.isAuthenticated) {
      Navigator.of(context).pushReplacementNamed('/home');
    }
  },
  child: const LoginForm(),
)

Filter when to listen:

FeatureListener<TodoFeature, TodoState>(
  listenWhen: (previous, current) {
    // Only listen when error changes
    return previous.error != current.error;
  },
  listener: (context, state) {
    if (state.error != null) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(state.error!)),
      );
    }
  },
  child: const TodoList(),
)

Common Use Cases

  • Navigation: Push/pop routes when state changes
  • Dialogs: Show/hide dialogs based on state
  • Snackbars: Display messages on errors or success
  • Analytics: Track state transitions

4. FeatureSelector #

Purpose: Rebuilds UI only when a specific part of state changes, optimizing performance.

When to use: When you only care about a subset of state and want to avoid unnecessary rebuilds.

Usage

FeatureSelector<TodoFeature, TodoState, int>(
  selector: (state) => state.completedCount,
  builder: (context, completedCount) {
    return Text('Completed: $completedCount');
  },
)

Complex selection:

FeatureSelector<UserFeature, UserState, UserProfile?>(
  selector: (state) => state.user?.profile,
  builder: (context, profile) {
    if (profile == null) return const CircularProgressIndicator();
    return Text('Welcome, ${profile.name}!');
  },
)

Why use FeatureSelector?

Consider a small TodoState with several independent fields — UI that displays only the completed count should not rebuild when unrelated fields change:

final class TodoState {
  const TodoState({
    required this.todos,
    required this.completedCount,
    required this.isLoading,
    this.error,
  });

  final List<String> todos;
  final int completedCount;
  final bool isLoading;
  final String? error;
}

Without FeatureSelector (using FeatureBuilder) the widget rebuilds when ANY field on TodoState changes:

FeatureBuilder<TodoFeature, TodoState>(
  builder: (context, state) {
    return Text('Completed: ${state.completedCount}');
  },
)
// Rebuilds whenever ANY part of TodoState changes (todos, isLoading, error, etc.)

With FeatureSelector you pick a specific field — here completedCount — so the widget rebuilds only when that value changes:

FeatureSelector<TodoFeature, TodoState, int>(
  selector: (state) => state.completedCount,
  builder: (context, completedCount) {
    return Text('Completed: $completedCount');
  },
)
// Rebuilds ONLY when completedCount changes

5. FeatureEffectListener #

Purpose: Listens to effects emitted by the feature and performs UI-related side effects.

When to use: When effects need to trigger UI actions (navigation, dialogs, snackbars) that cannot be handled in EffectHandler.

Usage

sealed class TodoEffect {}
final class ShowSuccessMessage extends TodoEffect {
  const ShowSuccessMessage(this.message);
  final String message;
}
final class NavigateToDetail extends TodoEffect {
  const NavigateToDetail(this.todoId);
  final String todoId;
}

// In your widget tree:
FeatureEffectListener<TodoFeature, TodoEffect, ShowSuccessMessage>(
  listener: (context, effect) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(effect.message)),
    );
  },
  child: FeatureEffectListener<TodoFeature, TodoEffect, NavigateToDetail>(
    listener: (context, effect) {
      Navigator.of(context).pushNamed('/todo/${effect.todoId}');
    },
    child: const TodoList(),
  ),
)

When to use EffectHandler vs FeatureEffectListener

Use EffectHandler Use FeatureEffectListener
Business logic side effects (HTTP, storage, timers) UI-only side effects (navigation, dialogs, snackbars)
Testable with mocks Depends on Flutter context
Returns messages to feature No return value
Lives in feature setup Lives in widget tree

Example:

// ✅ EffectHandler: HTTP call (business logic)
sealed class TodoEffect {}
final class FetchTodos extends TodoEffect {}

final class FetchTodosHandler implements EffectHandler<TodoEffect, TodoMessage> {
  @override
  Future<void> call(TodoEffect effect, MsgEmitter<TodoMessage> emit) async {
    switch (effect) {
      case FetchTodos():
        final todos = await repository.fetchTodos();
        emit(TodosLoaded(todos));
    }
  }
}

// ✅ FeatureEffectListener: Navigation (UI)
sealed class TodoEffect {}
final class NavigateToDetail extends TodoEffect {
  const NavigateToDetail(this.todoId);
  final String todoId;
}

FeatureEffectListener<TodoFeature, TodoEffect, NavigateToDetail>(
  listener: (context, effect) {
    Navigator.of(context).pushNamed('/todo/${effect.todoId}');
  },
  child: const TodoList(),
)

Combining Widgets #

You can compose widgets to handle both state changes and effects:

FeatureProvider<TodoFeature>(
  create: (context) => todoFeature,
  child: FeatureListener<TodoFeature, TodoState>(
    listenWhen: (previous, current) => previous.error != current.error,
    listener: (context, state) {
      if (state.error != null) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(state.error!)),
        );
      }
    },
    child: FeatureEffectListener<TodoFeature, TodoEffect, NavigateToDetail>(
      listener: (context, effect) {
        Navigator.of(context).pushNamed('/todo/${effect.todoId}');
      },
      child: FeatureBuilder<TodoFeature, TodoState>(
        builder: (context, state) {
          if (state.isLoading) return const CircularProgressIndicator();
          return TodoList(todos: state.todos);
        },
      ),
    ),
  ),
)

Best Practices #

1. Define a feature typedef #

Reduces verbosity when declaring widget types:

typedef CounterFeature = Feature<CounterState, CounterMessage, CounterEffect>;

// Instead of:
FeatureBuilder<Feature<CounterState, CounterMessage, CounterEffect>, CounterState>(...)

// Use:
FeatureBuilder<CounterFeature, CounterState>(...)

2. Use FeatureSelector for performance #

When displaying a small part of a large state object:

// ❌ BAD: Rebuilds on ANY state change
FeatureBuilder<UserFeature, UserState>(
  builder: (context, state) => Text(state.user.name),
)

// ✅ GOOD: Rebuilds ONLY when name changes
FeatureSelector<UserFeature, UserState, String>(
  selector: (state) => state.user.name,
  builder: (context, name) => Text(name),
)

3. Keep UI side effects in FeatureEffectListener #

Navigation, dialogs, and snackbars should be handled in the UI layer, not in EffectHandler:

// ✅ GOOD: Navigation in FeatureEffectListener
FeatureEffectListener<MyFeature, MyEffect, NavigateToHome>(
  listener: (context, effect) {
    Navigator.of(context).pushNamed('/home');
  },
  child: const MyWidget(),
)

4. Use FeatureListener for one-time UI actions #

When you need to respond to state changes without rebuilding UI:

FeatureListener<AuthFeature, AuthState>(
  listener: (context, state) {
    if (state.isAuthenticated) {
      Navigator.of(context).pushReplacementNamed('/home');
    }
  },
  child: const LoginPage(),
)

5. Filter rebuilds with buildWhen and listenWhen #

Avoid unnecessary work by filtering state changes:

FeatureBuilder<TodoFeature, TodoState>(
  buildWhen: (previous, current) {
    // Only rebuild when todos list changes, ignore loading flag
    return previous.todos != current.todos;
  },
  builder: (context, state) => TodoList(todos: state.todos),
)

Packages #

Package Pub Description
puer pub package Core TEA implementation with Feature, update, and effect handlers. Pure Dart foundation.
puer_flutter pub package Flutter integration: FeatureProvider, FeatureBuilder, FeatureListener widgets.
puer_effect_handlers pub package Composable wrappers for debouncing, sequential execution, and isolate offloading.
puer_test pub package Testing utilities for concise update and handler tests. Add to dev_dependencies.
puer_time_travel pub package Time-travel debugging with DevTools extension. Use in debug builds to inspect history.

Learn More #


License #

MIT

1
likes
160
points
192
downloads

Documentation

API reference

Publisher

verified publishervorky.io

Weekly Downloads

A bunch of widgets that helps you to easily works with puer with Flutter.

Homepage
Repository (GitHub)
View/report issues

Topics

#state-management #architecture #unidirectional-data-flow #mvi

License

MIT (license)

Dependencies

flutter, meta, provider, puer

More

Packages that depend on puer_flutter