🐕

ChangeNotifierはProviderで、StateNotifierはRiverpod.

2024/04/02に公開

参考記事。
https://zenn.dev/flutteruniv/books/flutter-architecture/viewer/3_mvvm

ChangeNotifierとStateNotifier、だいたい使われ方似てるのでメモ

ChangeNotifierはProviderで、StateNotifierはRiverpod.

ChangeNotifier

Flutterのfoundationライブラリに含まれています。
notifyListeners()メソッドを呼び出すことで、リスナー(ウィジェットなど)に変更を通知します。
ミュータブルな状態を持つことが一般的です。つまり、オブジェクトのプロパティを直接変更することで状態を更新します。
リスナーは変更された全体のオブジェクトに対して通知を受け取りますが、どの部分が変更されたかは特定できません。
大規模なアプリケーションや多くの状態を持つアプリケーションでは、パフォーマンスの問題が発生する可能性があります。

class CounterModel with ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CounterModel(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    // context.watchを使用してCounterModelの状態を購読
    final counter = context.watch<CounterModel>();

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('ChangeNotifier Example')),
        body: Center(
          child: Text('Count: ${counter.count}'),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => context.read<CounterModel>().increment(),
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}


// final counter = context.watch<CounterModel>();を使わずに、
// Consumer<CounterModel>の書き方もある

StateNotifier

river_podパッケージ(またはflutter_riverpod)に含まれています。これは、より宣言的な状態管理を目指して設計されたライブラリです。
不変の状態を持ち、状態の更新は新しい状態オブジェクトの作成によって行われます。これにより、状態の変更がより予測可能になります。
StateNotifierはジェネリッククラスであり、状態の型を明示的に指定します。これにより、型安全性が向上します。
状態の変更は、新しい状態オブジェクトをstateプロパティに割り当てることでリスナーに通知されます。
StateNotifierを使用すると、状態の変更がより細かく制御でき、大規模なアプリケーションでもパフォーマンスの問題が少なくなります。

// Model
class CounterModel {
  int count;

  CounterModel(this.count);
}

// ViewModel
class CounterViewModel extends StateNotifier<CounterModel> {
  CounterViewModel() : super(CounterModel(0));

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

  void reset() {
    state = CounterModel(0);
  }
}

final counterViewModelProvider = StateNotifierProvider<CounterViewModel, CounterModel>((ref) {
  return CounterViewModel();
});


//View
class CounterPage extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final counterState = ref.watch(counterViewModelProvider);

    return Scaffold(
      appBar: AppBar(title: Text('MVVM Counter App')),
      body: Center(
        child: Text('Count: ${counterState.count}', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterViewModelProvider.notifier).increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}

// final counter = context.watch<CounterModel>();
// を使わずに、
// Widget build(BuildContext context, WidgetRef ref) {
// final counterState = ref.watch(counterViewModelProvider);
// refを使う。

riverpodアノテーションを利用した例

// counter_model.dart
class CounterModel {
  final int count;
  CounterModel(this.count);
}

// counter_view_model.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'counter_model.dart';

part 'counter_view_model.g.dart';


class CounterViewModel extends _$CounterViewModel {
  CounterViewModel(Ref ref) : super(ref, CounterModel(0));

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

  void reset() {
    state = CounterModel(0);
  }
}

// View
class CounterPage extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final counterState = ref.watch(counterViewModelProvider);

    return Scaffold(
      appBar: AppBar(title: Text('MVVM Counter App')),
      body: Center(
        child: Text('Count: ${counterState.count}', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterViewModelProvider.notifier).increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}

アノテーションを使うと、この部分を.g.dartでやってくれる。

final counterViewModelProvider = StateNotifierProvider<CounterViewModel, CounterModel>((ref) {
  return CounterViewModel();
});

アノテーションのだいたいのメリットは、この部分をviewmodelに書かなくて良くなるのと、引数を渡しやすくなる。
familyを使えば、アノテーションを使わなくても引数は渡せる。

Discussion