ChangeNotifierを使ったObserverパターンの実装

## はじめに
Flutterで開発していくと、イベント、状態管理、再描画が密接になり、コードを切り離そうとしてもうまく切り離せない事がしばしば。そこで、UIとビジネスロジックを分離しつつ、効率的に状態管理できるObserverパターンを導入します。
FlutterではChangeNotifierを使って実装する事が一般的なため、サンプルコードを実装していきます。

## そもそもObserverパターンって何？？
Observerパターン（監視者パターン）は、オブジェクト指向プログラミングにおけるデザインパターンの一つです。このパターンは、一つのオブジェクト（被観測者/Subject）が持つデータや状態が変更されたときに、その変更を他のオブジェクト（監視者/Observer）に自動的に通知する仕組みを提供します。


### Observerパターンの主な特徴
1.	対象（Subject）
　•状態を持つオブジェクトで、状態が変わると監視者に通知を行います
2.	監視者（Observer）
　•対象の状態を監視しているオブジェクトで、対象から通知を受け取ると、自身の状態や振る舞いを更新します。
 3.	通知の仕組み：
　•被観測者（Subject）は自分の状態が変更されると、すべての監視者（Observer）にその変更を通知します。これにより、監視者は最新の状態に基づいて自身の動作を変更できます。

### Observerパターンの流れ
	1.	登録：監視者（Observer）は、被観測者（Subject）に対して「状態の変更を知りたい」と登録します。
	2.	状態変更：被観測者（Subject）の状態が何らかの理由で変更されます。
	3.	通知：被観測者（Subject）は、自分に登録されているすべての監視者（Observer）に「状態が変わったよ！」と通知します。
	4.	更新：通知を受け取った監視者（Observer）は、新しい状態を反映して自分自身を更新します。

### Observerパターンのメリット
　•疎結合：SubjectとObserverは独立しており、ObserverはSubjectの実装に依存しません。このため、システムの柔軟性が向上します。
　•自動更新：監視者は、被観測者からの通知を受けることで、自動的に最新の状態に基づいた処理を行います。これにより、手動での状態管理や同期の必要が減ります。

 ## 設計

```mermaid:
classDiagram
    ChangeNotifier <|-- CounterProvider
    CounterProvider o-- MyModel
    CounterProvider <.. HomeScreen : Uses
    HomeScreen --> CustomButton : Uses

    class ChangeNotifier {
        +List~Observer~ observers
        +notifyListeners() void
    }

    class CounterProvider {
        -int _counter
        +int get counter
        +void increment()
    }

    class MyModel {
        -int _value
        +int get value
        +void increment()
        +void decrement()
    }

    class HomeScreen {
        +CounterProvider counterProvider
        +build() Widget
    }

    class CustomButton {
        -String label
        -VoidCallback onPressed
        +build() Widget
    }
```

```shell:ディレクトリ構成
~/develop/changenotifier_counter (master)$ tree lib 
lib
├── main.dart
├── models
│   └── counter.dart
├── providers
│   └── counter_provider.dart
├── screens
│   └── home.dart
└── widgets
    └── custom_button.dart

5 directories, 5 files
```

 ## 成果物
counterアプリを作成します。
![counter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/4c8db68f-e13f-43d5-6e8c-6a611a0e916b.gif)

https://github.com/kouji0705/changenotifier_counter

## プロダクトコード

```dart:lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/counter_provider.dart';
import 'screens/home.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<CounterProvider>(
      create: (_) => CounterProvider(),
      child: MaterialApp(
        title: 'ChangeNotifier Counter',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: HomeScreen(),
      ),
    );
  }
}
```
ここでは利用予定のProviderを設定しています。


```dart:lib/screens/home.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/counter_provider.dart';
import '../widgets/custom_button.dart'; // CustomButtonをインポート

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterProvider = Provider.of<CounterProvider>(context); //状態管理を行うためにProviderパッケージを使用する

    return Scaffold(
      appBar: AppBar(
        title: Text('Counter: ${counterProvider.counter}'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${counterProvider.counter}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          CustomButton(
            label: 'Increment',
            onPressed: counterProvider.increment,
          ),
          SizedBox(width: 10), // ボタン間のスペース
          CustomButton(
            label: 'Decrement',
            onPressed: counterProvider.decrement,
          ),
        ],
      ),
    );
  }
}
```
→画面全体を表示する画面です。ここでは、状態管理Providerの設定、CustomButtonを配置しています。

```dart:lib/widgets/custom_button.dart
import 'package:flutter/material.dart';

class CustomButton extends StatelessWidget {
  final String label;
  final VoidCallback onPressed;

  CustomButton({required this.label, required this.onPressed});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      child: Text(label),
    );
  }
}

```
→カスタムボタンを設定しています。

```dart:lib/providers/counter_provider.dart
// lib/providers/counter_provider.dart
import 'package:flutter/material.dart';
import '../models/counter.dart';

class CounterProvider extends ChangeNotifier {
  final Counter _counter = Counter();

  int get counter => _counter.count;

  void increment() {
    _counter.increment();
    notifyListeners(); // 値の変更を通知
  }

  void decrement() {
    _counter.decrement();
    notifyListeners(); // 値の変更を通知
  }
}
```
→値の変更を通知しています。ここでは通知とcounterの値を更新する処理をしています。
Counterの状態管理のincrementやdecrementなどのビジネスロジックは、Counterで定義しています。

```dart:lib/models/counter.dart
class Counter {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
  }

  void decrement() {
    _count--;
  }
}
```
→ここではカウンターに関する状態と状態を変更する動作が実装されています。

## テストコードの実装

```shell:ディレクトリ構成
~/develop/changenotifier_counter$ tree test 
test
├── providers
│   └── counter_provider_test.dart
├── screens
│   └── home_test.dart
└── widgets
    └── custom_button.dart
```

```dart:test/screens/home_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:changenotifier_counter/providers/counter_provider.dart';
import 'package:changenotifier_counter/screens/home.dart';

void main() {
  testWidgets('HomeScreenのウィジェットテスト increment decrement',
      (WidgetTester tester) async {
    // Arrange: CounterProviderを提供し、HomeScreenをビルド
    await tester.pumpWidget(
      ChangeNotifierProvider(
        create: (_) => CounterProvider(),
        child: MaterialApp(
          home: HomeScreen(),
        ),
      ),
    );

    // Assert: 初期状態でカウンターの値が「0」であることを確認
    expect(find.text('0'), findsOneWidget);
    expect(find.text('You have pushed the button this many times:'),
        findsOneWidget);

    // Act: Incrementボタンをタップ
    await tester.tap(find.text('Increment'));
    await tester.pump(); // ウィジェットをリビルド

    // Assert: カウンターの値が「1」になることを確認
    expect(find.text('1'), findsOneWidget);

    // Act: Decrementボタンをタップ
    await tester.tap(find.text('Decrement'));
    await tester.pump(); // ウィジェットをリビルド

    // Assert: カウンターの値が再び「0」になることを確認
    expect(find.text('0'), findsOneWidget);
  });
}
```

```dart:test/widgets/custom_button.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:changenotifier_counter/widgets/custom_button.dart';

void main() {
  testWidgets('CustomButtonのウィジェットテスト', (WidgetTester tester) async {
    // Arrange
    var wasPressed = false;
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: CustomButton(
            label: 'Test Button',
            onPressed: () {
              wasPressed = true;
            },
          ),
        ),
      ),
    );

    // Act
    await tester.tap(find.text('Test Button'));
    await tester.pump(); // ウィジェットをリビルド

    // Assert
    expect(wasPressed, isTrue); // ボタンが押されたかどうかを確認
  });
}
```

```dart:test/providers/counter_provider_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:changenotifier_counter/providers/counter_provider.dart';

void main() {
  group('CounterProvider Tests', () {
    test('初期値が0であることを確認', () {
      // Arrange
      final counterProvider = CounterProvider();
      // Assert
      expect(counterProvider.counter, 0);
    });

    test('increment時、1になることを確認', () {
      // Arrange
      final counterProvider = CounterProvider();

      // Act
      counterProvider.increment();

      // Assert
      expect(counterProvider.counter, 1);
    });

    test('decrement時、-1になることを確認', () {
      // Arrange
      final counterProvider = CounterProvider();
      // Act
      counterProvider.decrement();

      // Assert
      expect(counterProvider.counter, -1);
    });
  });
}
```



### 感想
ObserperパターンはReactやバックエンドのコードでは書いていたんですが、Flutterでは初でした。
勉強になりました！！！