はじめに
Flutterで開発していくと、イベント、状態管理、再描画が密接になり、コードを切り離そうとしてもうまく切り離せない事がしばしば。そこで、UIとビジネスロジックを分離しつつ、効率的に状態管理できるObserverパターンを導入します。
FlutterではChangeNotifierを使って実装する事が一般的なため、サンプルコードを実装していきます。
そもそもObserverパターンって何??
Observerパターン(監視者パターン)は、オブジェクト指向プログラミングにおけるデザインパターンの一つです。このパターンは、一つのオブジェクト(被観測者/Subject)が持つデータや状態が変更されたときに、その変更を他のオブジェクト(監視者/Observer)に自動的に通知する仕組みを提供します。
Observerパターンの主な特徴
- 対象(Subject)
•状態を持つオブジェクトで、状態が変わると監視者に通知を行います - 監視者(Observer)
•対象の状態を監視しているオブジェクトで、対象から通知を受け取ると、自身の状態や振る舞いを更新します。 - 通知の仕組み:
•被観測者(Subject)は自分の状態が変更されると、すべての監視者(Observer)にその変更を通知します。これにより、監視者は最新の状態に基づいて自身の動作を変更できます。
Observerパターンの流れ
1. 登録:監視者(Observer)は、被観測者(Subject)に対して「状態の変更を知りたい」と登録します。
2. 状態変更:被観測者(Subject)の状態が何らかの理由で変更されます。
3. 通知:被観測者(Subject)は、自分に登録されているすべての監視者(Observer)に「状態が変わったよ!」と通知します。
4. 更新:通知を受け取った監視者(Observer)は、新しい状態を反映して自分自身を更新します。
Observerパターンのメリット
•疎結合:SubjectとObserverは独立しており、ObserverはSubjectの実装に依存しません。このため、システムの柔軟性が向上します。
•自動更新:監視者は、被観測者からの通知を受けることで、自動的に最新の状態に基づいた処理を行います。これにより、手動での状態管理や同期の必要が減ります。
設計
~/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
成果物
プロダクトコード
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を設定しています。
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を配置しています。
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),
);
}
}
→カスタムボタンを設定しています。
// 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で定義しています。
class Counter {
int _count = 0;
int get count => _count;
void increment() {
_count++;
}
void decrement() {
_count--;
}
}
→ここではカウンターに関する状態と状態を変更する動作が実装されています。
テストコードの実装
~/develop/changenotifier_counter$ tree test
test
├── providers
│ └── counter_provider_test.dart
├── screens
│ └── home_test.dart
└── widgets
└── custom_button.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);
});
}
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); // ボタンが押されたかどうかを確認
});
}
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では初でした。
勉強になりました!!!