[Flutter] [Pragmatic State Management in Flutter (Google I/O'19)]で推奨されてたProvider使ってみた

[Pragmatic State Management in Flutter (Google I/O'19)](https://www.youtube.com/watch?v=d_m5csmrf7I&t=1s)の中でこれからFlutter始めるなら[provider](https://pub.dev/packages/provider)を推奨するって言われてたので少しだけ触ってみました。

今回はChangeNotifierProviderのみです。

## Providerとは
> A dependency injection system built with widgets for widgets. provider is mostly syntax sugar for InheritedWidget, to make common use-cases straightforward.

READMEに書かれている通り、ウィジェット用のDIするものです。

下記のように親で提供したものが子孫で使えるようになり、引数で渡し続けなくて良くなります。
reactのcontext APIのproviderとかreact-reduxのproviderとかと同じ感じだと思います。

```dart
void main() => runApp(
      Provider<String>.value(
        value: 'Hello World', // これが子孫で使えるようになる
        child: MaterialApp(
          home: Child(),
        ),
      ),
    );

class Child extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Grandchild();
  }
}

class Grandchild extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(Provider.of<String>(context)); // 今回はここで使用している。下位ウィジェットでも同様に取得できる。
  }
}
```

## ChangeNotifierProviderを利用した実装

下のgifのような動きをするアプリケーションをChangeNotifierProviderを利用して実装しただけのものです。
![provider.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117867/61fc8232-c36b-b28e-cdc0-abbfb7aab5a1.gif)

コードは下記になります。
流れとしては、「ChangeNotifierProviderがChangeNotifierをmixinしたCounterを子孫ウィジェットに提供する。Counterは状態変更したらnotifyListenersで変更通知を行う。変更通知により再レンダリングが行われる。」って感じです。

適宜ChangeNotifyProviderが関連する箇所にコメント入れてあります。

### 実装
```dart:lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:first_flutter_provider_example/counter.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ChangeNotifierProvider<Counter>( // ChangeNotifierProviderを利用して子孫ウィジェットでCounterを使えるようにする
        builder: (_) => Counter(0),
        child: MyHomePage(title: 'First Flutter Provider Example'),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final String title;
  MyHomePage({Key key, @required this.title}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    var counter = Provider.of<Counter>(context); // 上位のウィジェットから提供されているCounterを取得
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              '${counter.value}', // counterを表示
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            onPressed: counter.increment, // counterをインクリメント
            child: Icon(Icons.add),
          ),
          SizedBox(height: 10),
          FloatingActionButton(
            onPressed: counter.decrement, // counterをデクリメント
            child: Icon(Icons.remove),
          )
        ],
      ),
    );
  }
}

```

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

class Counter with ChangeNotifier { // ChangeNotifierをmixin
  int _value;

  Counter(this._value);

  int get value => this._value;

  increment() {
    _value++;
    notifyListeners(); // 変更通知により再描画
  }

  decrement() {
    _value--;
    notifyListeners(); // 変更通知により再描画
  }
}

```

### テスト

```dart:test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:first_flutter_provider_example/main.dart';

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());

    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });

  testWidgets('Counter decrements smoke test', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());

    expect(find.text('0'), findsOneWidget);
    expect(find.text('-1'), findsNothing);

    await tester.tap(find.byIcon(Icons.remove));
    await tester.pump();

    expect(find.text('0'), findsNothing);
    expect(find.text('-1'), findsOneWidget);
  });
}
```

## 終わりに
次はprovider使ったBlocをやってみようかと思います。


今回のリポジトリは[こちら](https://github.com/hiko1129/first-flutter-provider-example)