パッケージ、リポジトリ、アプリカテゴリを検索

パッケージ詳細v1.0.0

state_notifier

Flutterの外にあるValueNotifier、そしていくつかの追加機能付き

AndroidiOSWebmacOSWindowsLinux
30日間ダウンロード
397万
いいね数
338
Pub ポイント
160

パッケージをインストール

flutter pub add state_notifier

パッケージ概要

公開情報に基づく選定ガイドです。参考としてご利用ください。

StateNotifier は、Flutter フレームワーク外での状態管理に拡張された機能を備えた ValueNotifier を拡張する Dart パッケージです。非同期処理、エラー処理、デバッグツールの組み込みサポートを備えたクリーンでリアクティブな状態管理方法を提供します。Web、デスクトップ、サーバーサイドアプリケーションなどの非 Flutter 環境での使用を想定しており、依存性注入と良好に統合され、関心の分離を促進します。不変の更新と簡素化されたサブスクリプションにより、パフォーマンスと開発者の体験が向上しています。

用途

  • Flutter以外のDartアプリケーションでのアプリケーション状態の管理
  • エラー回復を用いた非同期状態遷移の処理
  • 依存関係のインジェクションシステムとの統合
  • 複数プラットフォーム間で再利用可能な状態ロジックの構築
  • テスト可能で保守可能な状態クラスの作成

主な機能

  • ValueNotifier を拡張して追加機能を提供
  • 非同期の状態更新とエラーをサポートしています
  • 不変状態の更新
  • 組み込みのデバッグユーティリティ
  • プラットフォームに依存しない設計
  • DIフレームワークとの簡単な統合

適している対象

  • クロスプラットフォームアプリを構築するDart開発者
  • 複数のプラットフォーム間で共有状態ロジックを必要とするチーム
  • Flutter を使わないで堅牢な状態管理を必要とするプロジェクト
  • 軽量でテスト可能な状態ソリューションを求める開発者

注意事項

  • Flutterウィジェット内で直接使用することを想定していません
  • 手動での購読管理が必要です
  • 依存関係の挿入システムと最適に組み合わせる
  • 限定された組み込みUI統合
  • 複雑な状態フローには追加のボイラープレートが必要になる可能性があります

元の README

関連するソースリポジトリの README 原文のスナップショットです。最新の内容はリポジトリをご覧ください。

pub package Welcome to state_notifier~

This package is a recommended solution for managing state when using Provider or Riverpod.

Long story short, instead of extending ChangeNotifier, extend StateNotifier:

class City {
  City({required this.name, required this.population});
  final String name;
  final int population;
}


class CityNotifier extends StateNotifier<List<City>> {
  CityNotifier() : super(const <City>[]);

  void addCity(City newCity) {
    state = [
      ...state,
      newCity,
    ];
  }
}

Motivation

The purpose of StateNotifier is to be a simple solution to control state in an immutable manner.
While ChangeNotifier is simple, through its mutable nature, it can be harder to maintain as it grows larger.

By using immutable state, it becomes a lot simpler to:

  • compare previous and new state
  • implement an undo-redo mechanism
  • debug the application state

Good practices

DON'T update the state of a StateNotifier outside the notifier

While you could technically write:

class Counter extends StateNotifier<int> {
  Counter(): super(0);
}

final notifier = Counter();
notifier.state++;

That is considered an anti-pattern (and your IDE should show a warning).

Only the StateNotifier should modify its state. Instead, prefer using a method:

class Counter extends StateNotifier<int> {
  Counter(): super(0);

  void increment() => state++:
}

final notifier = Counter();
notifier.increment();

The goal is to centralize all the logic that modifies a StateNotifier within the StateNotifier itself.

FAQ

Why are listeners called when the new state is == to the previous state?

You may realize that a StateNotifier does not use == to verify that the state has changed before notifying for changes.

This behavior is voluntary, for performance reasons.

The reasoning is that StateNotifier is typically used with complex objects, which often override == to perform a deep comparison.
But performing a deep comparison can be a costly operation, especially since it is common for the state to contain lists/maps.
Similarly, for complex states, it is rare that when calling notifier.state = newState, the new and previous states are the same.

As such, instead of using ==, StateNotifier relies on identical to compare objects.
This way, when using StateNotifier with simple states like int/enums, it will correctly filter identical states. At the same time, this preserves performance on complex states, as identical will not perform a deep object comparison.

Using custom notification filter logic

You can override the method updateShouldNotify(T old,T current) of a StateNotifier to change the default behaviour, such as for:

  • using == instead of identical to filter updates, for deep state comparison
  • always returning true to revert to older behaviors of StateNotifier
  @override
  bool updateShouldNotify(User old, User current) {
    /// only update the User content changes, even if using a different instance
    return old.name != current.name && old.age != current.age;
  }

Usage

Integration with Freezed

While entirely optional, it is recommended to use StateNotifier in combination with Freezed.
Freezed is a code-generation package for data-classes in Dart, which automatically generates methods like copyWith and adds support for union-types.

A typical example would be using Freezed to handle data vs error vs loading states. With its union-types, it can lead to a significant improvement in maintainability as it:

  • ensures that your application will not enter illogical states (such as both having a "data" and being in the "loading" state)
  • ensures that logic handles all possible cases. Such as forcing that the loading/error cases be checked before trying to access the data.

The idea is that, rather than defining the data, error and loading state in a single object like:

class MyState {
  MyState(...);
  final Data data;
  final Object? error;
  final bool loading;
}

We can use Freezed to define it as:

@freezed
class MyState {
  factory MyState.data(Data data) = MyStateData;
  factory MyState.error(Object? error) = MyStateError;
  factory MyState.loading() = MyStateLoading;
}

That voluntarily prevents us from doing:

void main() {
  MyState state;
  print(state.data);
}

Instead, we can use the generated map method to handle the various cases:

void main() {
  MyState state;
  state.when(
    data: (state) => print(state.data),
    loading: (state) => print('loading'),
    error: (state) => print('Error: ${state.error}'),
  );
}
Integration with provider/service locators

StateNotifier is easily compatible with provider through an extra mixin: LocatorMixin.

Consider a typical StateNotifier:

class Count {
  Count(this.count);
  final int count;
}

class Counter extends StateNotifier<Count> {
  Counter(): super(Count(0));

  void increment() {
    state = Count(state.count + 1);
  }
}

In this example, we may want to use Provider.of/context.read to connect our Counter with external services.

To do so, simply mix-in LocatorMixin as such:

class Counter extends StateNotifier<Count> with LocatorMixin {
// unchanged
}

That then gives you access to:

  • read, a function to obtain services
  • update, a new life-cycle that can be used to listen to changes on a service

We could use them to change our Counter incrementation to save the counter in a DB when incrementing the value:

class Counter extends StateNotifier<Count> with LocatorMixin {
  Counter(): super(Count(0))

  void increment() {
    state = Count(state.count + 1);
    read<LocalStorage>().writeInt('count', state.count);
  }
}

Where Counter and LocalStorage are defined using provider this way:

void main() {
  runApp(
    MultiProvider(
      providers: [
        Provider(create: (_) => LocalStorage()),
        StateNotifierProvider<Counter, Count>(create: (_) => Counter()),
      ],
      child: MyApp(),
    ),
  );
}

Then, Counter/Count are consumed using your typical context.watch/Consumer/context.select/...:

@override
Widget build(BuildContext context) {
  int count = context.watch<Count>().count;

  return Scaffold(
    body: Text('$count'),
    floatingActionButton: FloatingActionButton(
      onPressed: () => context.read<Counter>().increment(),
      child: Icon(Icons.add),
    ),
  );
}

Testing

When using LocatorMixin, you may want to mock a dependency for your tests.
Of course, we still don't want to depend on Flutter/provider to do such a thing.

Similarly, since state is protected, tests need a simple way to read the state.

As such, LocatorMixin also adds extra utilities to help you with this scenario:

myStateNotifier.debugMockDependency<MyDependency>(myDependency);
print(myStateNotifier.debugState);
myStateNotifier.debugUpdate();

As such, if we want to test our previous Counter, we could mock LocalStorage this way:

test('increment and saves to local storage', () {
  final mockLocalStorage = MockLocalStorage();
  final counter = Counter()
    ..debugMockDependency<LocalStorage>(mockLocalStorage);

  expect(counter.debugState, 0);

  counter.increment(); // works fine since we mocked the LocalStorage

  expect(counter.debugState, 1);
  // mockito stuff
  verify(mockLocalStorage.writeInt('int', 1));
});

Note: LocatorMixin only works on StateNotifier. If you try to use it on other classes by using with LocatorMixin, it will not work.

Sponsors

カテゴリー

依存関係

依存関係一覧 1 件

ソースリポジトリ

このパッケージを使用しているプロジェクト

ランキング